This commit is contained in:
Bill Burke 2016-12-02 19:25:17 -05:00
parent 9e50a45b4c
commit e88af874ca
34 changed files with 820 additions and 514 deletions

View file

@ -21,6 +21,8 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Replaces any ${} strings with their corresponding system property.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ -32,7 +34,7 @@ public final class EnvUtil {
}
/**
* Replaces any ${} strings with their corresponding environent variable.
* Replaces any ${} strings with their corresponding system property.
*
* @param val
* @return

View file

@ -10,4 +10,5 @@ Login and go to the User Federation tab and you should now see your deployed pro
Add the provider, save it, then any new user you create will be stored and in the custom store you implemented. You
can modify the example and hot deploy it using the above maven command again.
This example uses the built in in-memory datasource that comes with keycloak: ExampleDS.
This example uses the built in in-memory datasource that comes with keycloak: ExampleDS. NOTE!!! You'll have
to change this to be an xa datasource for this to work.

View file

@ -5,7 +5,7 @@
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="user-storage-jpa-example" transaction-type="JTA">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<jta-data-source>java:jboss/datasources/KeycloakDS</jta-data-source>
<class>org.keycloak.examples.storage.user.UserEntity</class>

View file

@ -1,4 +1,4 @@
Example User Federation Provider
Example User Storage Provider
===================================================
This is an example of user storage backed by a simple properties file. This properties file only contains username/password
@ -7,13 +7,10 @@ key pairs. To deploy this provider you must have Keycloak running in standalone
mvn clean install wildfly:deploy
The "readonly-property-file" provider is hardcoded to look within the users.properties file embeded in the deployment jar
THere is one user 'tbrady' with a password of 'superbowl'
The ClasspathPropertiesStorageProvider 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 "writeable-property-file" provider can be configured to point to a property file on disk. It uses federated
storage to augment the property file with any other information the user wants.
The FilePropertiesStorageProvider is an example 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.
Our developer guide walks through the implementation of both of these providers.

View file

@ -1,69 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.federation.properties;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class BasePropertiesStorageFactory<T extends BasePropertiesStorageProvider> implements UserStorageProviderFactory<T> {
protected ConcurrentHashMap<String, Properties> files = new ConcurrentHashMap<String, Properties>();
@Override
public T create(KeycloakSession session, ComponentModel model) {
// first get the path to our properties file from the stored configuration of this provider instance.
String path = model.getConfig().getFirst("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 (T)createProvider(session, new UserStorageProviderModel(model), props);
props = new Properties();
InputStream is = getPropertiesFileStream(path);
if (is != null) {
try {
props.load(is);
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// remember the properties file for next time
files.put(path, props);
return (T)createProvider(session, new UserStorageProviderModel(model), props);
}
protected abstract InputStream getPropertiesFileStream(String path);
protected abstract BasePropertiesStorageProvider createProvider(KeycloakSession session, UserStorageProviderModel model, Properties props);
}

View file

@ -1,72 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.federation.properties;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.UserStorageProviderModel;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClasspathPropertiesStorageFactory extends BasePropertiesStorageFactory<ClasspathPropertiesStorageProvider> {
public static final String PROVIDER_NAME = "classpath-properties";
protected static final List<ProviderConfigProperty> configProperties;
static {
configProperties = ProviderConfigurationBuilder.create()
.property().name("path")
.type(ProviderConfigProperty.STRING_TYPE)
.label("Classpath")
.helpText("Classpath of properties file")
.add().build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
protected BasePropertiesStorageProvider createProvider(KeycloakSession session, UserStorageProviderModel model, Properties props) {
return new ClasspathPropertiesStorageProvider(session, model, props);
}
protected InputStream getPropertiesFileStream(String path) {
InputStream is = getClass().getClassLoader().getResourceAsStream(path);
if (is == null) {
throw new IllegalStateException("Path not found for properties file");
}
return is;
}
@Override
public String getId() {
return PROVIDER_NAME;
}
}

View file

@ -1,72 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.federation.properties;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.adapter.AbstractUserAdapter;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
/**
* Creates a read-only user. Implements CredentialInputUpdater so that passwords cannot be updated
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClasspathPropertiesStorageProvider extends BasePropertiesStorageProvider implements CredentialInputUpdater {
public ClasspathPropertiesStorageProvider(KeycloakSession session, UserStorageProviderModel model, Properties properties) {
super(session, model, properties);
}
@Override
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapter(session, realm, model) {
@Override
public String getUsername() {
return username;
}
};
}
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (input.getType().equals(CredentialModel.PASSWORD)) throw new ReadOnlyException("user is read only for this update");
return false;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return Collections.EMPTY_SET;
}
}

View file

@ -1,77 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.federation.properties;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.UserStorageProviderModel;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class FilePropertiesStorageFactory extends BasePropertiesStorageFactory<FilePropertiesStorageProvider> {
public static final String PROVIDER_NAME = "file-properties";
protected static final List<ProviderConfigProperty> configProperties;
static {
configProperties = ProviderConfigurationBuilder.create()
.property().name("path")
.type(ProviderConfigProperty.STRING_TYPE)
.label("Path")
.helpText("File path to properties file")
.add().build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
protected BasePropertiesStorageProvider createProvider(KeycloakSession session, UserStorageProviderModel model, Properties props) {
return new FilePropertiesStorageProvider(session, props, model);
}
protected InputStream getPropertiesFileStream(String path) {
try {
return new FileInputStream(path);
} catch (FileNotFoundException e) {
return null;
}
}
/**
* 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 PROVIDER_NAME;
}
}

View file

@ -1,129 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.federation.properties;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
import org.keycloak.storage.user.UserRegistrationProvider;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class FilePropertiesStorageProvider extends BasePropertiesStorageProvider implements
UserRegistrationProvider,
CredentialInputUpdater
{
public FilePropertiesStorageProvider(KeycloakSession session, Properties properties, UserStorageProviderModel model) {
super(session, model, properties);
}
public void save() {
String path = model.getConfig().getFirst("path");
try {
FileOutputStream fos = new FileOutputStream(path);
properties.store(fos, "");
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
synchronized (properties) {
if (properties.remove(user.getUsername()) == null) return false;
save();
return true;
}
}
@Override
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
throw new ReadOnlyException("Cannot edit username");
}
};
}
@Override
public UserModel addUser(RealmModel realm, String username) {
synchronized (properties) {
properties.setProperty(username, UNSET_PASSWORD);
save();
}
return createAdapter(realm, username);
}
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
if (!cred.getType().equals(CredentialModel.PASSWORD)) return false;
synchronized (properties) {
properties.setProperty(user.getUsername(), cred.getValue());
save();
}
return true;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!credentialType.equals(CredentialModel.PASSWORD)) return;
synchronized (properties) {
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
save();
}
}
private static final Set<String> disableableTypes = new HashSet<>();
static {
disableableTypes.add(CredentialModel.PASSWORD);
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return disableableTypes;
}
}

View file

@ -15,56 +15,52 @@
* limitations under the License.
*/
package org.keycloak.examples.federation.properties;
package org.keycloak.examples.userstorage.readonly;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.adapter.AbstractUserAdapter;
import org.keycloak.storage.user.UserLookupProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class BasePropertiesStorageProvider implements
public class PropertyFileUserStorageProvider implements
UserStorageProvider,
UserLookupProvider,
CredentialInputValidator
CredentialInputValidator,
CredentialInputUpdater
{
public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";
protected KeycloakSession session;
protected Properties properties;
protected UserStorageProviderModel model;
protected ComponentModel model;
// map of loaded users in this transaction
protected Map<String, UserModel> loadedUsers = new HashMap<>();
public BasePropertiesStorageProvider(KeycloakSession session, UserStorageProviderModel model, Properties properties) {
public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
this.session = session;
this.model = model;
this.properties = properties;
}
public KeycloakSession getSession() {
return session;
}
public Properties getProperties() {
return properties;
}
// UserLookupProvider methods
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
@ -79,6 +75,15 @@ public abstract class BasePropertiesStorageProvider implements
return adapter;
}
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapter(session, realm, model) {
@Override
public String getUsername() {
return username;
}
};
}
@Override
public UserModel getUserById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
@ -86,44 +91,18 @@ public abstract class BasePropertiesStorageProvider implements
return getUserByUsername(username, realm);
}
protected abstract UserModel createAdapter(RealmModel realm, String username);
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
return null;
}
@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
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
// complete we dont'care if a role is removed
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
String password = properties.getProperty(user.getUsername());
if (password == null || password.equals(UNSET_PASSWORD)) return false;
return password.equals(cred.getValue());
}
// CredentialInputValidator methods
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
String password = properties.getProperty(user.getUsername());
return credentialType.equals(CredentialModel.PASSWORD) && password != null && !password.equals(UNSET_PASSWORD);
return credentialType.equals(CredentialModel.PASSWORD) && password != null;
}
@Override
@ -131,6 +110,36 @@ public abstract class BasePropertiesStorageProvider implements
return credentialType.equals(CredentialModel.PASSWORD);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
String password = properties.getProperty(user.getUsername());
if (password == null) return false;
return password.equals(cred.getValue());
}
// CredentialInputUpdater methods
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (input.getType().equals(CredentialModel.PASSWORD)) throw new ReadOnlyException("user is read only for this update");
return false;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return Collections.EMPTY_SET;
}
@Override
public void close() {

View file

@ -0,0 +1,69 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.userstorage.readonly;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PropertyFileUserStorageProviderFactory implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
public static final String PROVIDER_NAME = "readonly-property-file";
protected Properties properties = new Properties();
@Override
public String getId() {
return PROVIDER_NAME;
}
@Override
public void init(Config.Scope config) {
InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties");
if (is == null) {
logger.warn("Could not find users.properties in classpath");
} else {
try {
properties.load(is);
} catch (IOException ex) {
logger.error("Failed to load users.properties file", ex);
}
}
}
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
}

View file

@ -0,0 +1,299 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.userstorage.writeable;
import org.keycloak.common.util.EnvUtil;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PropertyFileUserStorageProvider implements
UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
CredentialInputUpdater,
UserRegistrationProvider,
UserQueryProvider {
public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";
protected KeycloakSession session;
protected Properties properties;
protected ComponentModel model;
// map of loaded users in this transaction
protected Map<String, UserModel> loadedUsers = new HashMap<>();
public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
this.session = session;
this.model = model;
this.properties = properties;
}
// UserLookupProvider methods
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
UserModel adapter = loadedUsers.get(username);
if (adapter == null) {
String password = properties.getProperty(username);
if (password != null) {
adapter = createAdapter(realm, username);
loadedUsers.put(username, adapter);
}
}
return adapter;
}
protected UserModel createAdapter(RealmModel realm, String username) {
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
String pw = (String)properties.remove(username);
if (pw != null) {
properties.put(username, pw);
save();
}
}
};
}
@Override
public UserModel getUserById(String id, RealmModel realm) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(username, realm);
}
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
return null;
}
// UserQueryProvider methods
@Override
public int getUsersCount(RealmModel realm) {
return properties.size();
}
@Override
public List<UserModel> getUsers(RealmModel realm) {
return getUsers(realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
List<UserModel> users = new LinkedList<>();
int i = 0;
for (Object obj : properties.keySet()) {
if (i++ < firstResult) continue;
String username = (String)obj;
UserModel user = getUserByUsername(username, realm);
users.add(user);
if (users.size() >= maxResults) break;
}
return users;
}
// UserQueryProvider method implementations
@Override
public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search, realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
List<UserModel> users = new LinkedList<>();
int i = 0;
for (Object obj : properties.keySet()) {
String username = (String)obj;
if (!username.contains(search)) continue;
if (i++ < firstResult) continue;
UserModel user = getUserByUsername(username, realm);
users.add(user);
if (users.size() >= maxResults) break;
}
return users;
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
return searchForUser(params, realm, 0, Integer.MAX_VALUE);
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
// only support searching by username
String usernameSearchString = params.get("username");
if (usernameSearchString == null) return Collections.EMPTY_LIST;
return searchForUser(usernameSearchString, realm, firstResult, maxResults);
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
// runtime automatically handles querying UserFederatedStorage
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
// runtime automatically handles querying UserFederatedStorage
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
// runtime automatically handles querying UserFederatedStorage
return Collections.EMPTY_LIST;
}
// UserRegistrationProvider method implementations
public void save() {
String path = model.getConfig().getFirst("path");
path = EnvUtil.replace(path);
try {
FileOutputStream fos = new FileOutputStream(path);
properties.store(fos, "");
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public UserModel addUser(RealmModel realm, String username) {
synchronized (properties) {
properties.setProperty(username, UNSET_PASSWORD);
save();
}
return createAdapter(realm, username);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
synchronized (properties) {
if (properties.remove(user.getUsername()) == null) return false;
save();
return true;
}
}
// CredentialInputValidator methods
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
String password = properties.getProperty(user.getUsername());
return credentialType.equals(CredentialModel.PASSWORD) && password != null;
}
@Override
public boolean supportsCredentialType(String credentialType) {
return credentialType.equals(CredentialModel.PASSWORD);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
String password = properties.getProperty(user.getUsername());
if (password == null || UNSET_PASSWORD.equals(password)) return false;
return password.equals(cred.getValue());
}
// CredentialInputUpdater methods
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false;
if (!input.getType().equals(CredentialModel.PASSWORD)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
synchronized (properties) {
properties.setProperty(user.getUsername(), cred.getValue());
save();
}
return true;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!credentialType.equals(CredentialModel.PASSWORD)) return;
synchronized (properties) {
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
save();
}
}
private static final Set<String> disableableTypes = new HashSet<>();
static {
disableableTypes.add(CredentialModel.PASSWORD);
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return disableableTypes;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.userstorage.writeable;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.EnvUtil;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PropertyFileUserStorageProviderFactory implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
public static final String PROVIDER_NAME = "writeable-property-file";
protected static final List<ProviderConfigProperty> configMetadata;
static {
configMetadata = ProviderConfigurationBuilder.create()
.property().name("path")
.type(ProviderConfigProperty.STRING_TYPE)
.label("Path")
.defaultValue("${jboss.server.config.dir}/example-users.properties")
.helpText("File path to properties file")
.add().build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configMetadata;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
String fp = config.getConfig().getFirst("path");
if (fp == null) throw new ComponentValidationException("user property file does not exist");
fp = EnvUtil.replace(fp);
File file = new File(fp);
if (!file.exists()) {
throw new ComponentValidationException("user property file does not exist");
}
}
@Override
public String getId() {
return PROVIDER_NAME;
}
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
String path = model.getConfig().getFirst("path");
path = EnvUtil.replace(path);
Properties props = new Properties();
try {
InputStream is = new FileInputStream(path);
props.load(is);
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return new PropertyFileUserStorageProvider(session, model, props);
}
}

View file

@ -1,2 +1,2 @@
org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
org.keycloak.examples.userstorage.readonly.PropertyFileUserStorageProviderFactory
org.keycloak.examples.userstorage.writeable.PropertyFileUserStorageProviderFactory

View file

@ -1,20 +0,0 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
#Sun Aug 03 10:52:57 EDT 2014
belichick=superbowl

View file

@ -25,7 +25,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPStorageProvider;

View file

@ -1810,6 +1810,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
em.flush();
setConfig(component, c);
ComponentUtil.notifyCreated(session, this, component);
}

View file

@ -1706,6 +1706,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
}
updateRealm();
ComponentUtil.notifyCreated(session, this, model);
}

View file

@ -89,5 +89,9 @@ public class ComponentUtil {
ComponentFactory factory = getComponentFactory(session, model);
factory.onCreate(session, realm, model);
}
public static void notifyUpdated(KeycloakSession session, RealmModel realm, ComponentModel model) {
ComponentFactory factory = getComponentFactory(session, model);
factory.onUpdate(session, realm, model);
}
}

View file

@ -39,13 +39,22 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
return null;
}
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException;
default
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException
{
}
default
void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
default
void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
/**
* These are config properties that are common across all implementation of this component type
*

View file

@ -17,6 +17,8 @@
package org.keycloak.credential;
/**
*
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/

View file

@ -22,6 +22,10 @@ import org.keycloak.models.UserModel;
import java.util.List;
/**
* Implentations of this interface can validate CredentialInput, i.e. verify a password.
* UserStorageProviders and CredentialProviders can implement this interface.
*
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/

View file

@ -22,6 +22,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
/**
* All these methods effect an entire cluster of Keycloak instances.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ -39,5 +41,10 @@ public interface UserCache extends UserProvider {
* @param realm
*/
void evict(RealmModel realm);
/**
* Clear cache entirely.
*
*/
void clear();
}

View file

@ -27,6 +27,8 @@ import java.util.Map;
import java.util.Set;
/**
* Delegation pattern. Used to proxy UserModel implementations.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/

View file

@ -21,6 +21,8 @@ import java.util.Arrays;
import java.util.List;
/**
* Configuration property metadata. Used to render generic configuration pages for Keycloak extensions in the admin console.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ -28,17 +30,23 @@ public class ProviderConfigProperty {
public static final String BOOLEAN_TYPE="boolean";
public static final String STRING_TYPE="String";
// Possibility to configure multiple String values of any value (something like "redirect_uris" for clients)
/**
* Possibility to configure multiple String values of any value (something like "redirect_uris" for clients)
*/
public static final String MULTIVALUED_STRING_TYPE="MultivaluedString";
public static final String SCRIPT_TYPE="Script";
public static final String FILE_TYPE="File";
public static final String ROLE_TYPE="Role";
// Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select)
/**
* Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select)
*/
public static final String LIST_TYPE="List";
// Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple)
/**
* Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple)
*/
public static final String MULTIVALUED_LIST_TYPE="MultivaluedList";
public static final String CLIENT_LIST_TYPE="ClientList";
@ -77,6 +85,11 @@ public class ProviderConfigProperty {
this.secret = secret;
}
/**
* Name of the config variable stored in the database
*
* @return
*/
public String getName() {
return name;
}
@ -85,6 +98,11 @@ public class ProviderConfigProperty {
this.name = name;
}
/**
* Label shown in the admin console when configuring the variable
*
* @return
*/
public String getLabel() {
return label;
}
@ -93,6 +111,12 @@ public class ProviderConfigProperty {
this.label = label;
}
/**
* Type of the variable. i.e. boolean, string etc. See the constants declared in this class for what your choices
* are.
*
* @return
*/
public String getType() {
return type;
}
@ -101,6 +125,11 @@ public class ProviderConfigProperty {
this.type = type;
}
/**
* Default value for the variable
*
* @return
*/
public Object getDefaultValue() {
return defaultValue;
}
@ -109,6 +138,11 @@ public class ProviderConfigProperty {
this.defaultValue = defaultValue;
}
/**
* For list types, this is a list of choices to choose from.
*
* @return
*/
public List<String> getOptions() {
return options;
}
@ -117,6 +151,11 @@ public class ProviderConfigProperty {
this.options = options;
}
/**
* Help text that will be displayed in the admin console tooltip
*
* @return
*/
public String getHelpText() {
return helpText;
}
@ -125,6 +164,12 @@ public class ProviderConfigProperty {
this.helpText = helpText;
}
/**
* If true, this variable is only writeable. It will never be viewable. This is important for things like
* passwords in which you never want to display them on the screen.
*
* @return
*/
public boolean isSecret() {
return secret;
}

View file

@ -22,6 +22,9 @@ import java.util.LinkedList;
import java.util.List;
/**
* Builds a list of ProviderConfigProperty instances.
*
*
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ProviderConfigurationBuilder {
@ -58,6 +61,11 @@ public class ProviderConfigurationBuilder {
return this;
}
/**
* Create the list.
*
* @return
*/
public List<ProviderConfigProperty> build() {
return properties;
}
@ -77,42 +85,94 @@ public class ProviderConfigurationBuilder {
return this;
}
/**
* Label that will be shown for this configuration property in the admin console
*
* @param label
* @return
*/
public ProviderConfigPropertyBuilder label(String label) {
this.label = label;
return this;
}
/**
* Help text that will be shown for this configuration property in the admin console
* when you hover over the tooltip
*
* @param helpText
* @return
*/
public ProviderConfigPropertyBuilder helpText(String helpText) {
this.helpText = helpText;
return this;
}
/**
* Property type. i.e. boolean, string.
* @see ProviderConfigProperty
*
*
* @param type
* @return
*/
public ProviderConfigPropertyBuilder type(String type) {
this.type = type;
return this;
}
/**
* Default value that will be shown when configuring this property for the first time
*
* @param defaultValue
* @return
*/
public ProviderConfigPropertyBuilder defaultValue(Object defaultValue) {
this.defaultValue = defaultValue;
return this;
}
/**
* If configuring a list type, these are the options you can choose from.
*
* @param options
* @return
*/
public ProviderConfigPropertyBuilder options(String... options) {
this.options = Arrays.asList(options);
return this;
}
/**
* If configuring a list type, these are the options you can choose from.
*
* @param options
* @return
*/
public ProviderConfigPropertyBuilder options(List<String> options) {
this.options = options;
return this;
}
/**
* If turned on, this property is only writable and never readable.
* This is useful for things like passwords where you never want an admin
* to be able to see what the password is.
*
* @param secret
* @return
*/
public ProviderConfigPropertyBuilder secret(boolean secret) {
this.secret = secret;
return this;
}
/**
* Add the current property, and start building the next one
*
* @return
*/
public ProviderConfigurationBuilder add() {
ProviderConfigProperty property = new ProviderConfigProperty();
property.setName(name);

View file

@ -28,9 +28,40 @@ import org.keycloak.provider.Provider;
public interface UserStorageProvider extends Provider {
void preRemove(RealmModel realm);
void preRemove(RealmModel realm, GroupModel group);
void preRemove(RealmModel realm, RoleModel role);
/**
* Callback when a realm is removed. Implement this if, for example, you want to do some
* cleanup in your user storage when a realm is removed
*
* @param realm
*/
default
void preRemove(RealmModel realm) {
}
/**
* Callback when a group is removed. Allows you to do things like remove a user
* group mapping in your external store if appropriate
*
* @param realm
* @param group
*/
default
void preRemove(RealmModel realm, GroupModel group) {
}
/**
* Callback when a role is removed. Allows you to do things like remove a user
* role mapping in your external store if appropriate
* @param realm
* @param role
*/
default
void preRemove(RealmModel realm, RoleModel role) {
}
/**
* Optional type that can be used by implementations to

View file

@ -40,7 +40,7 @@ import java.util.Set;
* of "f:" + providerId + ":" + getUsername(). UserModel properties like enabled, firstName, lastName, email, etc. are all
* stored as attributes in federated storage.
*
* isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated
* isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated storage
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -116,6 +116,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return true;
}
/**
* Gets groups from federated storage and automatically appends default groups of realm.
* Also calls getGroupsInternal() method
* to pull group membership from provider. Implementors can override that method
*
*
* @return
*/
@Override
public Set<GroupModel> getGroups() {
Set<GroupModel> set = new HashSet<>();
@ -143,6 +151,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return RoleUtils.isMember(roles, group);
}
/**
* Gets role mappings from federated storage and automatically appends default roles.
* Also calls getRoleMappingsInternal() method
* to pull role mappings from provider. Implementors can override that method
*
*
* @return
*/
@Override
public Set<RoleModel> getRealmRoleMappings() {
Set<RoleModel> roleMappings = getRoleMappings();
@ -157,6 +173,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return realmRoles;
}
/**
* Gets role mappings from federated storage and automatically appends default roles.
* Also calls getRoleMappingsInternal() method
* to pull role mappings from provider. Implementors can override that method
*
*
* @return
*/
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
Set<RoleModel> roleMappings = getRoleMappings();
@ -202,6 +226,13 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return Collections.EMPTY_SET;
}
/**
* Gets role mappings from federated storage and automatically appends default roles.
* Also calls getRoleMappingsInternal() method
* to pull role mappings from provider. Implementors can override that method
*
* @return
*/
@Override
public Set<RoleModel> getRoleMappings() {
Set<RoleModel> set = new HashSet<>();
@ -343,6 +374,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return getFirstAttribute(FIRST_NAME_ATTRIBUTE);
}
/**
* Stores as attribute in federated storage.
* FIRST_NAME_ATTRIBUTE
*
* @param firstName
*/
@Override
public void setFirstName(String firstName) {
setSingleAttribute(FIRST_NAME_ATTRIBUTE, firstName);
@ -354,6 +391,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return getFirstAttribute(LAST_NAME_ATTRIBUTE);
}
/**
* Stores as attribute in federated storage.
* LAST_NAME_ATTRIBUTE
*
* @param lastName
*/
@Override
public void setLastName(String lastName) {
setSingleAttribute(LAST_NAME_ATTRIBUTE, lastName);
@ -365,6 +408,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
return getFirstAttribute(EMAIL_ATTRIBUTE);
}
/**
* Stores as attribute in federated storage.
* EMAIL_ATTRIBUTE
*
* @param email
*/
@Override
public void setEmail(String email) {
setSingleAttribute(EMAIL_ATTRIBUTE, email);
@ -378,6 +427,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
else return Boolean.valueOf(val);
}
/**
* Stores as attribute in federated storage.
* EMAIL_VERIFIED_ATTRIBUTE
*
* @param verified
*/
@Override
public void setEmailVerified(boolean verified) {
setSingleAttribute(EMAIL_VERIFIED_ATTRIBUTE, Boolean.toString(verified));

View file

@ -28,7 +28,7 @@ import org.keycloak.models.UserModel;
*/
public interface ImportedUserValidation {
/**
* If this method returns null, then the user storage in local storage will be removed
* If this method returns null, then the user in local storage will be removed
*
* @param realm
* @param user

View file

@ -20,6 +20,9 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
* Optional capability interface implemented by UserStorageProviders. This interface is required
* if you want the UserStorageProvider to support basic login capabilities.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/

View file

@ -24,6 +24,10 @@ import java.util.List;
import java.util.Map;
/**
* Optional capability interface implemented by UserStorageProviders.
* Defines complex queries that are used to locate one or more users. You must implement this interface
* if you want to view and manager users from the administration console.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ -99,10 +103,47 @@ public interface UserQueryProvider {
*/
List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults);
/**
* Get users that belong to a specific group. Implementations do not have to search in UserFederatedStorageProvider
* as this is done automatically.
*
* @see org.keycloak.storage.federated.UserFederatedStorageProvider
*
* @param realm
* @param group
* @param firstResult
* @param maxResults
* @return
*/
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
/**
* Get users that belong to a specific group. Implementations do not have to search in UserFederatedStorageProvider
* as this is done automatically.
*
* @see org.keycloak.storage.federated.UserFederatedStorageProvider
*
*
*
* @param realm
* @param group
* @return
*/
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group);
/**
* Search for users that have a specific attribute with a specific value.
* Implementations do not have to search in UserFederatedStorageProvider
* as this is done automatically.
*
* @see org.keycloak.storage.federated.UserFederatedStorageProvider
*
// Searching by UserModel.attribute (not property)
*
* @param attrName
* @param attrValue
* @param realm
* @return
*/
List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm);
}

View file

@ -21,6 +21,9 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
/**
* Optional capability interface implemented by UserStorageProviders.
* Implement this interface if your provider supports adding and removing users.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/

View file

@ -285,7 +285,9 @@ public class UserStorageManager implements UserProvider, OnUserCache {
protected List<UserModel> importValidation(RealmModel realm, List<UserModel> users) {
List<UserModel> tmp = new LinkedList<>();
for (UserModel user : users) {
tmp.add(importValidation(realm, user));
UserModel model = importValidation(realm, user);
if (model == null) continue;
tmp.add(model);
}
return tmp;
}