diff --git a/common/src/main/java/org/keycloak/common/util/reflections/Types.java b/common/src/main/java/org/keycloak/common/util/reflections/Types.java index e6787be5b5..adf8626ac3 100644 --- a/common/src/main/java/org/keycloak/common/util/reflections/Types.java +++ b/common/src/main/java/org/keycloak/common/util/reflections/Types.java @@ -17,7 +17,14 @@ package org.keycloak.common.util.reflections; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; +import java.util.Map; /** * Utility class for Types @@ -68,4 +75,564 @@ public class Types { return type; } } + + /** + * Is the genericType of a certain class? + */ + public static boolean isA(Class clazz, ParameterizedType pType) + { + return clazz.isAssignableFrom((Class) pType.getRawType()); + } + + /** + * Gets the index-th type argument. + */ + public static Class getArgumentType(ParameterizedType pType, int index) + { + return (Class) pType.getActualTypeArguments()[index]; + } + + public static Class getTemplateParameterOfInterface(Class base, Class desiredInterface) + { + Object rtn = searchForInterfaceTemplateParameter(base, desiredInterface); + if (rtn != null && rtn instanceof Class) return (Class) rtn; + return null; + } + + + private static Object searchForInterfaceTemplateParameter(Class base, Class desiredInterface) + { + for (int i = 0; i < base.getInterfaces().length; i++) + { + Class intf = base.getInterfaces()[i]; + if (intf.equals(desiredInterface)) + { + Type generic = base.getGenericInterfaces()[i]; + if (generic instanceof ParameterizedType) + { + ParameterizedType p = (ParameterizedType) generic; + Type type = p.getActualTypeArguments()[0]; + Class rtn = getRawTypeNoException(type); + if (rtn != null) return rtn; + return type; + } + else + { + return null; + } + } + } + if (base.getSuperclass() == null || base.getSuperclass().equals(Object.class)) return null; + Object rtn = searchForInterfaceTemplateParameter(base.getSuperclass(), desiredInterface); + if (rtn == null || rtn instanceof Class) return rtn; + if (!(rtn instanceof TypeVariable)) return null; + + String name = ((TypeVariable) rtn).getName(); + int index = -1; + TypeVariable[] variables = base.getSuperclass().getTypeParameters(); + if (variables == null || variables.length < 1) return null; + + for (int i = 0; i < variables.length; i++) + { + if (variables[i].getName().equals(name)) index = i; + } + if (index == -1) return null; + + + Type genericSuperclass = base.getGenericSuperclass(); + if (!(genericSuperclass instanceof ParameterizedType)) return null; + + ParameterizedType pt = (ParameterizedType) genericSuperclass; + Type type = pt.getActualTypeArguments()[index]; + + Class clazz = getRawTypeNoException(type); + if (clazz != null) return clazz; + return type; + } + + + /** + * See if the two methods are compatible, that is they have the same relative signature + * + * @param method + * @param intfMethod + * @return + */ + public static boolean isCompatible(Method method, Method intfMethod) + { + if (method == intfMethod) return true; + + if (!method.getName().equals(intfMethod.getName())) return false; + if (method.getParameterTypes().length != intfMethod.getParameterTypes().length) return false; + + for (int i = 0; i < method.getParameterTypes().length; i++) + { + Class rootParam = method.getParameterTypes()[i]; + Class intfParam = intfMethod.getParameterTypes()[i]; + if (!intfParam.isAssignableFrom(rootParam)) return false; + } + return true; + } + + /** + * Given a method and a root class, find the actual method declared in the root that implements the method. + * + * @param clazz + * @param intfMethod + * @return + */ + public static Method getImplementingMethod(Class clazz, Method intfMethod) + { + Class declaringClass = intfMethod.getDeclaringClass(); + if (declaringClass.equals(clazz)) return intfMethod; + + Class[] paramTypes = intfMethod.getParameterTypes(); + + if (declaringClass.getTypeParameters().length > 0 && paramTypes.length > 0) + { + Type[] intfTypes = findParameterizedTypes(clazz, declaringClass); + Map typeVarMap = new HashMap(); + TypeVariable>[] vars = declaringClass.getTypeParameters(); + for (int i = 0; i < vars.length; i++) + { + if (intfTypes != null && i < intfTypes.length) + { + typeVarMap.put(vars[i].getName(), intfTypes[i]); + } + else + { + // Interface type parameters may not have been filled out + typeVarMap.put(vars[i].getName(), vars[i].getGenericDeclaration()); + } + } + Type[] paramGenericTypes = intfMethod.getGenericParameterTypes(); + paramTypes = new Class[paramTypes.length]; + + for (int i = 0; i < paramTypes.length; i++) + { + if (paramGenericTypes[i] instanceof TypeVariable) + { + TypeVariable tv = (TypeVariable)paramGenericTypes[i]; + Type t = typeVarMap.get(tv.getName()); + if (t == null) + { + throw new RuntimeException("Unable to resolve type variable"); + } + paramTypes[i] = getRawType(t); + } + else + { + paramTypes[i] = getRawType(paramGenericTypes[i]); + } + } + + } + + try + { + return clazz.getMethod(intfMethod.getName(), paramTypes); + } + catch (NoSuchMethodException e) + { + } + + try + { + Method tmp = clazz.getMethod(intfMethod.getName(), intfMethod.getParameterTypes()); + return tmp; + } + catch (NoSuchMethodException e) + { + + } + return intfMethod; + } + + + public static Class getRawType(Type type) + { + if (type instanceof Class) + { + // type is a normal class. + return (Class) type; + + } + else if (type instanceof ParameterizedType) + { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type rawType = parameterizedType.getRawType(); + return (Class) rawType; + } + else if (type instanceof GenericArrayType) + { + final GenericArrayType genericArrayType = (GenericArrayType) type; + final Class componentRawType = getRawType(genericArrayType.getGenericComponentType()); + return Array.newInstance(componentRawType, 0).getClass(); + } + else if (type instanceof TypeVariable) + { + final TypeVariable typeVar = (TypeVariable) type; + if (typeVar.getBounds() != null && typeVar.getBounds().length > 0) + { + return getRawType(typeVar.getBounds()[0]); + } + } + throw new RuntimeException("unable to determine base class"); + } + + + public static Class getRawTypeNoException(Type type) + { + if (type instanceof Class) + { + // type is a normal class. + return (Class) type; + + } + else if (type instanceof ParameterizedType) + { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type rawType = parameterizedType.getRawType(); + return (Class) rawType; + } + else if (type instanceof GenericArrayType) + { + final GenericArrayType genericArrayType = (GenericArrayType) type; + final Class componentRawType = getRawType(genericArrayType.getGenericComponentType()); + return Array.newInstance(componentRawType, 0).getClass(); + } + return null; + } + + /** + * Returns the type argument from a parameterized type + * + * @param genericType + * @return null if there is no type parameter + */ + public static Class getTypeArgument(Type genericType) + { + if (!(genericType instanceof ParameterizedType)) return null; + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Class typeArg = (Class) parameterizedType.getActualTypeArguments()[0]; + return typeArg; + } + + + public static Class getCollectionBaseType(Class type, Type genericType) + { + if (genericType instanceof ParameterizedType) + { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Type componentGenericType = parameterizedType.getActualTypeArguments()[0]; + return getRawType(componentGenericType); + } + else if (genericType instanceof GenericArrayType) + { + final GenericArrayType genericArrayType = (GenericArrayType) genericType; + Type componentGenericType = genericArrayType.getGenericComponentType(); + return getRawType(componentGenericType); + } + else if (type.isArray()) + { + return type.getComponentType(); + } + return null; + } + + + public static Class getMapKeyType(Type genericType) + { + if (genericType instanceof ParameterizedType) + { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Type componentGenericType = parameterizedType.getActualTypeArguments()[0]; + return getRawType(componentGenericType); + } + return null; + } + + public static Class getMapValueType(Type genericType) + { + if (genericType instanceof ParameterizedType) + { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Type componentGenericType = parameterizedType.getActualTypeArguments()[1]; + return getRawType(componentGenericType); + } + return null; + } + + public static Type resolveTypeVariables(Class root, Type type) + { + if (type instanceof TypeVariable) + { + Type newType = resolveTypeVariable(root, (TypeVariable)type); + return (newType == null) ? type : newType; + } + else if (type instanceof ParameterizedType) + { + final ParameterizedType param = (ParameterizedType)type; + final Type[] actuals = new Type[param.getActualTypeArguments().length]; + for (int i = 0; i < actuals.length; i++) + { + Type newType = resolveTypeVariables(root, param.getActualTypeArguments()[i]); + actuals[i] = newType == null ? param.getActualTypeArguments()[i] : newType; + } + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() + { + return actuals; + } + + @Override + public Type getRawType() + { + return param.getRawType(); + } + + @Override + public Type getOwnerType() + { + return param.getOwnerType(); + } + }; + } + else if (type instanceof GenericArrayType) + { + GenericArrayType arrayType = (GenericArrayType)type; + final Type componentType = resolveTypeVariables(root, arrayType.getGenericComponentType()); + if (componentType == null) return type; + return new GenericArrayType() + { + @Override + public Type getGenericComponentType() + { + return componentType; + } + }; + } + else + { + return type; + } + } + + + /** + * Finds an actual value of a type variable. The method looks in a class hierarchy for a class defining the variable + * and returns the value if present. + * + * @param root + * @param typeVariable + * @return actual type of the type variable + */ + public static Type resolveTypeVariable(Class root, TypeVariable typeVariable) + { + if (typeVariable.getGenericDeclaration() instanceof Class) + { + Class classDeclaringTypeVariable = (Class) typeVariable.getGenericDeclaration(); + Type[] types = findParameterizedTypes(root, classDeclaringTypeVariable); + if (types == null) return null; + for (int i = 0; i < types.length; i++) + { + TypeVariable tv = classDeclaringTypeVariable.getTypeParameters()[i]; + if (tv.equals(typeVariable)) + { + return types[i]; + } + } + } + return null; + } + + + /** + * Given a class and an interfaces, go through the class hierarchy to find the interface and return its type arguments. + * + * @param classToSearch + * @param interfaceToFind + * @return type arguments of the interface + */ + public static Type[] getActualTypeArgumentsOfAnInterface(Class classToSearch, Class interfaceToFind) + { + Type[] types = findParameterizedTypes(classToSearch, interfaceToFind); + if (types == null) throw new RuntimeException("Unable to find type arguments"); + return types; + } + + private static final Type[] EMPTY_TYPE_ARRAY = {}; + + /** + * Search for the given interface or class within the root's class/interface hierarchy. + * If the searched for class/interface is a generic return an array of real types that fill it out. + * + * @param root + * @param searchedFor + * @return + */ + public static Type[] findParameterizedTypes(Class root, Class searchedFor) + { + if (searchedFor.isInterface()) + { + return findInterfaceParameterizedTypes(root, null, searchedFor); + } + return findClassParameterizedTypes(root, null, searchedFor); + } + + public static Type[] findClassParameterizedTypes(Class root, ParameterizedType rootType, Class searchedForClass) + { + if (Object.class.equals(root)) return null; + + Map typeVarMap = populateParameterizedMap(root, rootType); + + Class superclass = root.getSuperclass(); + Type genericSuper = root.getGenericSuperclass(); + + if (superclass.equals(searchedForClass)) + { + return extractTypes(typeVarMap, genericSuper); + } + + + if (genericSuper instanceof ParameterizedType) + { + ParameterizedType intfParam = (ParameterizedType) genericSuper; + Type[] types = findClassParameterizedTypes(superclass, intfParam, searchedForClass); + if (types != null) + { + return extractTypeVariables(typeVarMap, types); + } + } + else + { + Type[] types = findClassParameterizedTypes(superclass, null, searchedForClass); + if (types != null) + { + return types; + } + } + return null; + } + + private static Map populateParameterizedMap(Class root, ParameterizedType rootType) + { + Map typeVarMap = new HashMap(); + if (rootType != null) + { + TypeVariable>[] vars = root.getTypeParameters(); + for (int i = 0; i < vars.length; i++) + { + typeVarMap.put(vars[i].getName(), rootType.getActualTypeArguments()[i]); + } + } + return typeVarMap; + } + + + public static Type[] findInterfaceParameterizedTypes(Class root, ParameterizedType rootType, Class searchedForInterface) + { + Map typeVarMap = populateParameterizedMap(root, rootType); + + for (int i = 0; i < root.getInterfaces().length; i++) + { + Class sub = root.getInterfaces()[i]; + Type genericSub = root.getGenericInterfaces()[i]; + if (sub.equals(searchedForInterface)) + { + return extractTypes(typeVarMap, genericSub); + } + } + + for (int i = 0; i < root.getInterfaces().length; i++) + { + Type genericSub = root.getGenericInterfaces()[i]; + Class sub = root.getInterfaces()[i]; + + Type[] types = recurseSuperclassForInterface(searchedForInterface, typeVarMap, genericSub, sub); + if (types != null) return types; + } + if (root.isInterface()) return null; + + Class superclass = root.getSuperclass(); + Type genericSuper = root.getGenericSuperclass(); + + + return recurseSuperclassForInterface(searchedForInterface, typeVarMap, genericSuper, superclass); + } + + private static Type[] recurseSuperclassForInterface(Class searchedForInterface, Map typeVarMap, Type genericSub, Class sub) + { + if (genericSub instanceof ParameterizedType) + { + ParameterizedType intfParam = (ParameterizedType) genericSub; + Type[] types = findInterfaceParameterizedTypes(sub, intfParam, searchedForInterface); + if (types != null) + { + return extractTypeVariables(typeVarMap, types); + } + } + else + { + Type[] types = findInterfaceParameterizedTypes(sub, null, searchedForInterface); + if (types != null) + { + return types; + } + } + return null; + } + + private static Type[] extractTypeVariables(Map typeVarMap, Type[] types) + { + for (int j = 0; j < types.length; j++) + { + if (types[j] instanceof TypeVariable) + { + TypeVariable tv = (TypeVariable) types[j]; + types[j] = typeVarMap.get(tv.getName()); + } + else + { + types[j] = types[j]; + } + } + return types; + } + + private static Type[] extractTypes(Map typeVarMap, Type genericSub) + { + if (genericSub instanceof ParameterizedType) + { + ParameterizedType param = (ParameterizedType) genericSub; + Type[] types = param.getActualTypeArguments(); + Type[] returnTypes = new Type[types.length]; + System.arraycopy(types, 0, returnTypes, 0, types.length); + extractTypeVariables(typeVarMap, returnTypes); + return returnTypes; + } + else + { + return EMPTY_TYPE_ARRAY; + } + } + + /** + * Grabs the parameterized type of fromInterface + * that object implements and sees if it is assignable from type. + * + * @param type + * @param object + * @param fromInterface + * @param + * @return + */ + public static boolean supports(Class type, Object object, Class fromInterface) { + Type providerType = getActualTypeArgumentsOfAnInterface(object.getClass(), fromInterface)[0]; + Class providerClass = getRawType(providerType); + return type.isAssignableFrom(providerClass); + } + + } \ No newline at end of file diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index 4ec58bd02d..d64a3ead7e 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java @@ -147,6 +147,15 @@ public class JpaUserFederatedStorageProvider implements return result; } + @Override + public List getUsersByUserAttribute(RealmModel realm, String name, String value) { + TypedQuery query = em.createNamedQuery("getFederatedAttributesByNameAndValue", String.class) + .setParameter("realmId", realm.getId()) + .setParameter("name", name) + .setParameter("value", value); + return query.getResultList(); + } + @Override public String getUserByFederatedIdentity(FederatedIdentityModel link, RealmModel realm) { TypedQuery query = em.createNamedQuery("findUserByBrokerLinkAndRealm", String.class) @@ -508,6 +517,15 @@ public class JpaUserFederatedStorageProvider implements } + @Override + public List getMembership(RealmModel realm, GroupModel group, int firstResult, int max) { + TypedQuery query = em.createNamedQuery("fedgroupMembership", String.class) + .setParameter("realmId", realm.getId()) + .setParameter("groupId", group.getId()); + query.setFirstResult(firstResult); + query.setMaxResults(max); + return query.getResultList(); + } @Override public Set getRequiredActions(RealmModel realm, UserModel user) { @@ -689,7 +707,7 @@ public class JpaUserFederatedStorageProvider implements .setParameter("userId", user.getId()) .setParameter("realmId", realm.getId()) .executeUpdate(); - em.createNamedQuery("getFederatedUserRequiredActionsByUser") + em.createNamedQuery("deleteFederatedUserRequiredActionsByUser") .setParameter("userId", user.getId()) .setParameter("realmId", realm.getId()) .executeUpdate(); diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserAttributeEntity.java index 071eacdc75..c5e61932a7 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserAttributeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserAttributeEntity.java @@ -32,7 +32,7 @@ import javax.persistence.Table; * @version $Revision: 1 $ */ @NamedQueries({ - @NamedQuery(name="getFederatedAttributesByNameAndValue", query="select attr from FederatedUserAttributeEntity attr where attr.name = :name and attr.value = :value and attr.realmId=:realmId"), + @NamedQuery(name="getFederatedAttributesByNameAndValue", query="select attr.userId from FederatedUserAttributeEntity attr where attr.name = :name and attr.value = :value and attr.realmId=:realmId"), @NamedQuery(name="getFederatedAttributesByUser", query="select attr from FederatedUserAttributeEntity attr where attr.userId = :userId and attr.realmId=:realmId"), @NamedQuery(name="deleteUserFederatedAttributesByUser", query="delete from FederatedUserAttributeEntity attr where attr.userId = :userId and attr.realmId=:realmId"), @NamedQuery(name="deleteUserFederatedAttributesByUserAndName", query="delete from FederatedUserAttributeEntity attr where attr.userId = :userId and attr.name=:name and attr.realmId=:realmId"), diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java index ecfaa49ed1..3116a0ac44 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserGroupMembershipEntity.java @@ -35,7 +35,7 @@ import java.io.Serializable; @NamedQueries({ @NamedQuery(name="feduserMemberOf", query="select m from FederatedUserGroupMembershipEntity m where m.userId = :userId and m.groupId = :groupId"), @NamedQuery(name="feduserGroupMembership", query="select m from FederatedUserGroupMembershipEntity m where m.userId = :userId"), - @NamedQuery(name="fedgroupMembership", query="select g.userId from FederatedUserGroupMembershipEntity g where g.groupId = :groupId"), + @NamedQuery(name="fedgroupMembership", query="select g.userId from FederatedUserGroupMembershipEntity g where g.groupId = :groupId and g.realmId = :realmId"), @NamedQuery(name="feduserGroupIds", query="select m.groupId from FederatedUserGroupMembershipEntity m where m.userId = :userId"), @NamedQuery(name="deleteFederatedUserGroupMembershipByRealm", query="delete from FederatedUserGroupMembershipEntity mapping where mapping.realmId=:realmId"), @NamedQuery(name="deleteFederatedUserGroupMembershipByStorageProvider", query="delete from FederatedUserGroupMembershipEntity e where e.storageProviderId=:storageProviderId"), diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java index f555628344..f30aee6d6d 100755 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserRequiredActionEntity.java @@ -37,6 +37,7 @@ import java.io.Serializable; */ @NamedQueries({ @NamedQuery(name="getFederatedUserRequiredActionsByUser", query="select action from FederatedUserRequiredActionEntity action where action.userId = :userId and action.realmId=:realmId"), + @NamedQuery(name="deleteFederatedUserRequiredActionsByUser", query="delete from FederatedUserRequiredActionEntity action where action.realmId=:realmId and action.userId = :userId"), @NamedQuery(name="deleteFederatedUserRequiredActionsByRealm", query="delete from FederatedUserRequiredActionEntity action where action.realmId=:realmId"), @NamedQuery(name="deleteFederatedUserRequiredActionsByStorageProvider", query="delete from FederatedUserRequiredActionEntity e where e.storageProviderId=:storageProviderId"), @NamedQuery(name="deleteFederatedUserRequiredActionsByRealmAndLink", query="delete from FederatedUserRequiredActionEntity action where action.userId IN (select u.id from UserEntity u where u.realmId=:realmId and u.federationLink=:link)") diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageProviderFactory.java b/server-spi/src/main/java/org/keycloak/storage/StorageProviderFactory.java index 3369da2462..e4f27171b0 100755 --- a/server-spi/src/main/java/org/keycloak/storage/StorageProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/storage/StorageProviderFactory.java @@ -26,8 +26,7 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public interface StorageProviderFactory extends ProviderFactory { - boolean supports(Class type); +public interface StorageProviderFactory extends ProviderFactory { /** * called per Keycloak transaction. * @@ -35,14 +34,7 @@ public interface StorageProviderFactory extends ProviderFactory * @param model * @return */ - StorageProvider getInstance(KeycloakSession session, StorageProviderModel model); - - /** - * Config options to display in generic admin console page for federated - * - * @return - */ - Set getConfigurationOptions(); + T getInstance(KeycloakSession session, StorageProviderModel model); /** * This is the name of the provider and will be showed in the admin console as an option. diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java index 72e42800b6..db3919046d 100755 --- a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -18,6 +18,7 @@ package org.keycloak.storage; import org.jboss.logging.Logger; +import org.keycloak.common.util.reflections.Types; import org.keycloak.models.ClientModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.FederatedIdentityModel; @@ -42,6 +43,7 @@ import org.keycloak.models.utils.CredentialValidation; import org.keycloak.storage.federated.UserFederatedStorageProvider; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -79,18 +81,20 @@ public class UserStorageManager implements UserProvider { protected T getFirstStorageProvider(RealmModel realm, Class type) { for (StorageProviderModel model : getStorageProviders(realm)) { StorageProviderFactory factory = (StorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(StorageProvider.class, model.getProviderName()); - if (factory.supports(type)) { + + if (Types.supports(type, factory, StorageProviderFactory.class)) { return type.cast(factory.getInstance(session, model)); } } return null; } + protected List getStorageProviders(RealmModel realm, Class type) { List list = new LinkedList<>(); for (StorageProviderModel model : getStorageProviders(realm)) { StorageProviderFactory factory = (StorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(StorageProvider.class, model.getProviderName()); - if (factory.supports(type)) { + if (Types.supports(type, factory, StorageProviderFactory.class)) { list.add(type.cast(factory.getInstance(session, model))); } @@ -130,6 +134,7 @@ public class UserStorageManager implements UserProvider { @Override public boolean removeUser(RealmModel realm, UserModel user) { + getFederatedStorage().preRemove(realm, user); StorageId storageId = new StorageId(user.getId()); if (storageId.getProviderId() == null) { return localStorage().removeUser(realm, user); @@ -299,29 +304,33 @@ public class UserStorageManager implements UserProvider { return size; } + @FunctionalInterface interface PaginatedQuery { - List query(UserQueryProvider provider, int first, int max); + List query(Object provider, int first, int max); } protected List query(PaginatedQuery pagedQuery, RealmModel realm, int firstResult, int maxResults) { - List results = new LinkedList(); - if (maxResults == 0) return results; + if (maxResults == 0) return Collections.EMPTY_LIST; + List storageProviders = getStorageProviders(realm, UserQueryProvider.class); - LinkedList providers = new LinkedList<>(); - if (providers.isEmpty()) { + // we can skip rest of method if there are no storage providers + if (storageProviders.isEmpty()) { return pagedQuery.query(localStorage(), firstResult, maxResults); } + LinkedList providers = new LinkedList<>(); + List results = new LinkedList(); providers.add(localStorage()); providers.addAll(storageProviders); + providers.add(getFederatedStorage()); int leftToRead = maxResults; int leftToFirstResult = firstResult; - Iterator it = providers.iterator(); + Iterator it = providers.iterator(); while (it.hasNext() && leftToRead != 0) { - UserQueryProvider provider = it.next(); + Object provider = it.next(); boolean exhausted = false; int index = 0; if (leftToFirstResult > 0) { @@ -341,20 +350,22 @@ public class UserStorageManager implements UserProvider { results.addAll(tmp); if (leftToRead > 0) leftToRead -= tmp.size(); } + return results; } @Override public List getUsers(final RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) { - return query(new PaginatedQuery() { - @Override - public List query(UserQueryProvider provider, int first, int max) { - if (provider instanceof UserProvider) { // it is local storage - return ((UserProvider)provider).getUsers(realm, first, max, includeServiceAccounts); - } - return provider.getUsers(realm, first, max); + return query((provider, first, max) -> { + if (provider instanceof UserProvider) { // it is local storage + return ((UserProvider) provider).getUsers(realm, first, max, includeServiceAccounts); + } else if (provider instanceof UserQueryProvider) { + return ((UserQueryProvider)provider).getUsers(realm, first, max); + } - }, realm, firstResult, maxResults); + return Collections.EMPTY_LIST; + } + , realm, firstResult, maxResults); } @Override @@ -363,12 +374,13 @@ public class UserStorageManager implements UserProvider { } @Override - public List searchForUser(final String search, final RealmModel realm, int firstResult, int maxResults) { - return query(new PaginatedQuery() { - @Override - public List query(UserQueryProvider provider, int first, int max) { - return provider.searchForUser(search, realm, first, max); + public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { + return query((provider, first, max) -> { + if (provider instanceof UserQueryProvider) { + return ((UserQueryProvider)provider).searchForUser(search, realm, first, max); + } + return Collections.EMPTY_LIST; }, realm, firstResult, maxResults); } @@ -378,23 +390,36 @@ public class UserStorageManager implements UserProvider { } @Override - public List searchForUserByAttributes(final Map attributes, final RealmModel realm, int firstResult, int maxResults) { - return query(new PaginatedQuery() { - @Override - public List query(UserQueryProvider provider, int first, int max) { - return provider.searchForUserByAttributes(attributes, realm, first, max); + public List searchForUserByAttributes(Map attributes, RealmModel realm, int firstResult, int maxResults) { + return query((provider, first, max) -> { + if (provider instanceof UserQueryProvider) { + return ((UserQueryProvider)provider).searchForUserByAttributes(attributes, realm, first, max); + } - }, realm, firstResult, maxResults); + return Collections.EMPTY_LIST; + } + , realm, firstResult, maxResults); } @Override - public List searchForUserByUserAttribute(final String attrName, final String attrValue, RealmModel realm) { - return query(new PaginatedQuery() { - @Override - public List query(UserQueryProvider provider, int first, int max) { - return provider.searchForUserByUserAttribute(attrName, attrValue, realm); + public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { + List results = query((provider, first, max) -> { + if (provider instanceof UserQueryProvider) { + return ((UserQueryProvider)provider).searchForUserByUserAttribute(attrName, attrValue, realm); + + } else if (provider instanceof UserFederatedStorageProvider) { + List ids = ((UserFederatedStorageProvider)provider).getUsersByUserAttribute(realm, attrName, attrValue); + List rs = new LinkedList<>(); + for (String id : ids) { + UserModel user = getUserById(id, realm); + if (user != null) rs.add(user); + } + return rs; + } + return Collections.EMPTY_LIST; }, realm,0, Integer.MAX_VALUE - 1); + return results; } @Override @@ -432,12 +457,23 @@ public class UserStorageManager implements UserProvider { @Override public List getGroupMembers(final RealmModel realm, final GroupModel group, int firstResult, int maxResults) { - return query(new PaginatedQuery() { - @Override - public List query(UserQueryProvider provider, int first, int max) { - return provider.getGroupMembers(realm, group, first, max); + List results = query((provider, first, max) -> { + if (provider instanceof UserQueryProvider) { + return ((UserQueryProvider)provider).getGroupMembers(realm, group, first, max); + + } else if (provider instanceof UserFederatedStorageProvider) { + List ids = ((UserFederatedStorageProvider)provider).getMembership(realm, group, first, max); + List rs = new LinkedList(); + for (String id : ids) { + UserModel user = getUserById(id, realm); + if (user != null) rs.add(user); + } + return rs; + } + return Collections.EMPTY_LIST; }, realm, firstResult, maxResults); + return results; } diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java index 1d3ff73b46..45c616891d 100644 --- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java @@ -402,4 +402,19 @@ public abstract class AbstractUserAdapter implements UserModel { throw new ReadOnlyException("user is read only for this update"); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof UserModel)) return false; + + UserModel that = (UserModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + } diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java index bcc2651cb2..b09ceae6ca 100644 --- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java @@ -416,4 +416,19 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { getFederatedStorage().updateCredential(realm, this, cred); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof UserModel)) return false; + + UserModel that = (UserModel) o; + return that.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + } diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/UserAttributeFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/federated/UserAttributeFederatedStorage.java index 12bcabfcef..08755da564 100644 --- a/server-spi/src/main/java/org/keycloak/storage/federated/UserAttributeFederatedStorage.java +++ b/server-spi/src/main/java/org/keycloak/storage/federated/UserAttributeFederatedStorage.java @@ -22,6 +22,7 @@ import org.keycloak.models.UserModel; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -32,4 +33,5 @@ public interface UserAttributeFederatedStorage { void setAttribute(RealmModel realm, UserModel user, String name, List values); void removeAttribute(RealmModel realm, UserModel user, String name); MultivaluedHashMap getAttributes(RealmModel realm, UserModel user); + List getUsersByUserAttribute(RealmModel realm, String name, String value); } diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/UserGroupMembershipFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/federated/UserGroupMembershipFederatedStorage.java index fef3ceefcb..dad239948e 100644 --- a/server-spi/src/main/java/org/keycloak/storage/federated/UserGroupMembershipFederatedStorage.java +++ b/server-spi/src/main/java/org/keycloak/storage/federated/UserGroupMembershipFederatedStorage.java @@ -20,6 +20,7 @@ import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import java.util.List; import java.util.Set; /** @@ -28,9 +29,8 @@ import java.util.Set; */ public interface UserGroupMembershipFederatedStorage { Set getGroups(RealmModel realm, UserModel user); - void joinGroup(RealmModel realm,UserModel user, GroupModel group); - void leaveGroup(RealmModel realm,UserModel user, GroupModel group); + List getMembership(RealmModel realm, GroupModel group, int firstResult, int max); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java index 6e546a30f0..2cbf1bac19 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java @@ -38,6 +38,8 @@ import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; +import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -146,4 +148,89 @@ public class UserFederationStorageTest { loginSuccessAndLogout("thor", "lightning"); } + @Test + public void testQuery() { + KeycloakSession session = keycloakRule.startSession(); + RealmModel realm = session.realms().getRealmByName("test"); + + // Test paging + List localUsers = session.userLocalStorage().getUsers(realm, false); + Set queried = new HashSet<>(); + // tests assumes that local storage is queried first + int first = localUsers.size(); + while (queried.size() < 8) { + List results = session.users().getUsers(realm, first, 3); + if (results.size() == 0) break; + first += results.size(); + queried.addAll(results); + + } + Set usernames = new HashSet<>(); + for (UserModel user : queried) { + usernames.add(user.getUsername()); + System.out.println(user.getUsername()); + + } + Assert.assertEquals(8, queried.size()); + Assert.assertTrue(usernames.contains("thor")); + Assert.assertTrue(usernames.contains("zeus")); + Assert.assertTrue(usernames.contains("apollo")); + Assert.assertTrue(usernames.contains("perseus")); + Assert.assertTrue(usernames.contains("tbrady")); + Assert.assertTrue(usernames.contains("rob")); + Assert.assertTrue(usernames.contains("jules")); + Assert.assertTrue(usernames.contains("danny")); + + // test searchForUser + List users = session.users().searchForUser("tbrady", realm); + Assert.assertTrue(users.size() == 1); + Assert.assertTrue(users.get(0).getUsername().equals("tbrady")); + + // test getGroupMembers() + GroupModel gods = realm.createGroup("gods"); + UserModel user = null; + user = session.users().getUserByUsername("apollo", realm); + user.joinGroup(gods); + user = session.users().getUserByUsername("zeus", realm); + user.joinGroup(gods); + user = session.users().getUserByUsername("thor", realm); + user.joinGroup(gods); + queried.clear(); + usernames.clear(); + + first = 0; + while (queried.size() < 8) { + List results = session.users().getGroupMembers(realm, gods, first, 1); + if (results.size() == 0) break; + first += results.size(); + queried.addAll(results); + + } + for (UserModel u : queried) { + usernames.add(u.getUsername()); + System.out.println(u.getUsername()); + + } + Assert.assertEquals(3, queried.size()); + Assert.assertTrue(usernames.contains("apollo")); + Assert.assertTrue(usernames.contains("zeus")); + Assert.assertTrue(usernames.contains("thor")); + + // search by single attribute + System.out.println("search by single attribute"); + user = session.users().getUserByUsername("thor", realm); + user.setSingleAttribute("weapon", "hammer"); + + users = session.users().searchForUserByUserAttribute("weapon", "hammer", realm); + for (UserModel u : users) { + System.out.println(u.getUsername()); + + } + Assert.assertEquals(1, users.size()); + Assert.assertEquals("thor", users.get(0).getUsername()); + + + keycloakRule.stopSession(session, true); + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java index 52a43ec7d3..cf237e48d6 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java @@ -30,15 +30,19 @@ import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.storage.user.UserCredentialValidatorProvider; import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Properties; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class UserPropertyFileStorage implements UserLookupProvider, StorageProvider, UserCredentialValidatorProvider { +public class UserPropertyFileStorage implements UserLookupProvider, StorageProvider, UserCredentialValidatorProvider, UserQueryProvider { protected Properties userPasswords; protected StorageProviderModel model; @@ -128,6 +132,85 @@ public class UserPropertyFileStorage implements UserLookupProvider, StorageProvi return true; } + @Override + public int getUsersCount(RealmModel realm) { + return userPasswords.size(); + } + + @Override + public List getUsers(RealmModel realm) { + List users = new LinkedList<>(); + for (Object username : userPasswords.keySet()) { + users.add(createUser(realm, (String)username)); + } + return users; + } + + @Override + public List searchForUserByAttributes(Map attributes, RealmModel realm) { + return Collections.EMPTY_LIST; + } + + @Override + public List getUsers(RealmModel realm, int firstResult, int maxResults) { + if (maxResults == 0) return Collections.EMPTY_LIST; + List users = new LinkedList<>(); + int count = 0; + for (Object un : userPasswords.keySet()) { + if (count++ < firstResult) continue; + String username = (String)un; + users.add(createUser(realm, username)); + if (users.size() + 1 > maxResults) break; + } + return users; + } + + @Override + public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { + if (maxResults == 0) return Collections.EMPTY_LIST; + List users = new LinkedList<>(); + int count = 0; + for (Object un : userPasswords.keySet()) { + String username = (String)un; + if (username.contains(search)) { + if (count++ < firstResult) { + continue; + } + users.add(createUser(realm, username)); + if (users.size() + 1 > maxResults) break; + } + } + return users; + } + + @Override + public List searchForUserByAttributes(Map attributes, RealmModel realm, int firstResult, int maxResults) { + if (attributes.size() != 1) return Collections.EMPTY_LIST; + String username = attributes.get(UserModel.USERNAME); + if (username == null) return Collections.EMPTY_LIST; + return searchForUser(username, realm, firstResult, maxResults); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return Collections.EMPTY_LIST; + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return Collections.EMPTY_LIST; + } + + @Override + public List searchForUser(String search, RealmModel realm) { + return getUsers(realm, 0, Integer.MAX_VALUE - 1); + } + + @Override + public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { + return Collections.EMPTY_LIST; + } + @Override public void close() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java index d08065cd85..82d91cfd4f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java @@ -34,18 +34,13 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class UserPropertyFileStorageFactory implements StorageProviderFactory { +public class UserPropertyFileStorageFactory implements StorageProviderFactory { public static final String PROVIDER_ID = "user-password-props"; @Override - public boolean supports(Class type) { - return type.isAssignableFrom(UserPropertyFileStorage.class); - } - - @Override - public StorageProvider getInstance(KeycloakSession session, StorageProviderModel model) { + public UserPropertyFileStorage getInstance(KeycloakSession session, StorageProviderModel model) { Properties props = new Properties(); try { props.load(getClass().getResourceAsStream(model.getConfig().get("property.file"))); @@ -55,11 +50,6 @@ public class UserPropertyFileStorageFactory implements StorageProviderFactory { return new UserPropertyFileStorage(session, model, props); } - @Override - public Set getConfigurationOptions() { - return null; - } - @Override public String getId() { return PROVIDER_ID; diff --git a/testsuite/integration/src/test/resources/storage-test/read-only-user-password.properties b/testsuite/integration/src/test/resources/storage-test/read-only-user-password.properties index 1672680ecf..c0b76abe79 100644 --- a/testsuite/integration/src/test/resources/storage-test/read-only-user-password.properties +++ b/testsuite/integration/src/test/resources/storage-test/read-only-user-password.properties @@ -1 +1,4 @@ -tbrady=goat \ No newline at end of file +tbrady=goat +rob=pw +jules=pw +danny=pw diff --git a/testsuite/integration/src/test/resources/storage-test/user-password.properties b/testsuite/integration/src/test/resources/storage-test/user-password.properties index 9dd1c159d2..a6e28c1405 100644 --- a/testsuite/integration/src/test/resources/storage-test/user-password.properties +++ b/testsuite/integration/src/test/resources/storage-test/user-password.properties @@ -1 +1,4 @@ -thor=hammer \ No newline at end of file +thor=hammer +zeus=pw +apollo=pw +perseus=pw \ No newline at end of file