user storage provider jpa example

This commit is contained in:
Bill Burke 2016-08-03 19:16:11 -04:00
parent 3b9b673e5e
commit 70722d0d3d
32 changed files with 876 additions and 64 deletions

View file

@ -0,0 +1,45 @@
<!--
~ 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.
-->
<assembly>
<id>example-module</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>target</directory>
<outputDirectory>system/layers/keycloak/org/keycloak/examples/jpa-example/main</outputDirectory>
<filtered>true</filtered>
<includes>
<include>user-storage-jpa-example.jar</include>
</includes>
</fileSet>
<fileSet>
<directory>.</directory>
<outputDirectory>system/layers/keycloak/org/keycloak/examples/jpa-example/main</outputDirectory>
<filtered>true</filtered>
<includes>
<include>module.xml</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.examples.jpa-example">
<properties>
<property name="jboss.api" value="private"/>
</properties>
<resources>
<resource-root path="user-storage-jpa-example.jar"/>
</resources>
<dependencies>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-model-jpa"/>
<module name="javax.persistence.api"/>
<module name="org.jboss.logging"/>
<module name="org.javassist"/>
<module name="org.hibernate" services="import"/>
<module name="org.bouncycastle" />
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -74,6 +74,30 @@
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
<recompressZippedFiles>true</recompressZippedFiles>
<finalName>modules</finalName>
<appendAssemblyId>false</appendAssemblyId>
<outputDirectory>${project.build.directory}</outputDirectory>
<workDirectory>${project.build.directory}/assembly/work</workDirectory>
<tarLongFileMode>gnu</tarLongFileMode>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -16,27 +16,40 @@
*/
package org.keycloak.examples.storage.user;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
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.models.utils.KeycloakModelUtils;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.user.UserCredentialValidatorProvider;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ExampleUserStorageProvider implements UserStorageProvider, UserLookupProvider, UserRegistrationProvider {
public class ExampleUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
UserRegistrationProvider,
UserCredentialValidatorProvider,
UserQueryProvider {
private static final Logger logger = Logger.getLogger(ExampleUserStorageProvider.class);
protected EntityManager em;
protected ComponentModel model;
protected KeycloakSession session;
@ -69,18 +82,27 @@ public class ExampleUserStorageProvider implements UserStorageProvider, UserLook
@Override
public UserModel getUserById(String id, RealmModel realm) {
logger.info("getUserById: " + id);
String persistenceId = StorageId.externalId(id);
UserEntity entity = em.find(UserEntity.class, persistenceId);
if (entity == null) return null;
if (entity == null) {
logger.info("could not find user by id: " + id);
return null;
}
return new UserAdapter(session, realm, model, entity);
}
@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
logger.info("getUserByUsername: " + username);
TypedQuery<UserEntity> query = em.createNamedQuery("getUserByUsername", UserEntity.class);
query.setParameter("username", username);
List<UserEntity> result = query.getResultList();
if (result.isEmpty()) return null;
if (result.isEmpty()) {
logger.info("could not find username: " + username);
return null;
}
return new UserAdapter(session, realm, model, result.get(0));
}
@ -99,6 +121,7 @@ public class ExampleUserStorageProvider implements UserStorageProvider, UserLook
entity.setId(KeycloakModelUtils.generateId());
entity.setUsername(username);
em.persist(entity);
logger.info("added user: " + username);
return new UserAdapter(session, realm, model, entity);
}
@ -115,4 +138,91 @@ public class ExampleUserStorageProvider implements UserStorageProvider, UserLook
public void grantToAllUsers(RealmModel realm, RoleModel role) {
}
@Override
public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input) {
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
// If we override getCredentialsDirectly/updateCredentialsDirectly
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
// If you don't like this workaround, you can query the database every time to validate the password
for (UserCredentialModel cred : input) {
if (!UserCredentialModel.PASSWORD.equals(cred.getType())) return false;
if (!cred.getValue().equals(user.getFirstAttribute("password"))) return false;
}
return true;
}
@Override
public int getUsersCount(RealmModel realm) {
Object count = em.createNamedQuery("getUserCount")
.getSingleResult();
return ((Number)count).intValue();
}
@Override
public List<UserModel> getUsers(RealmModel realm) {
return getUsers(realm, -1, -1);
}
@Override
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
TypedQuery<UserEntity> query = em.createNamedQuery("getAllUsers", UserEntity.class);
if (firstResult != -1) {
query.setFirstResult(firstResult);
}
if (maxResults != -1) {
query.setMaxResults(maxResults);
}
List<UserEntity> results = query.getResultList();
List<UserModel> users = new LinkedList<>();
for (UserEntity entity : results) users.add(new UserAdapter(session, realm, model, entity));
return users;
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search, realm, -1, -1);
}
@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
TypedQuery<UserEntity> query = em.createNamedQuery("searchForUser", UserEntity.class);
query.setParameter("search", "%" + search.toLowerCase() + "%");
if (firstResult != -1) {
query.setFirstResult(firstResult);
}
if (maxResults != -1) {
query.setMaxResults(maxResults);
}
List<UserEntity> results = query.getResultList();
List<UserModel> users = new LinkedList<>();
for (UserEntity entity : results) users.add(new UserAdapter(session, realm, model, entity));
return users;
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return Collections.EMPTY_LIST;
}
@Override
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
return Collections.EMPTY_LIST;
}
}

View file

@ -16,25 +16,39 @@
*/
package org.keycloak.examples.storage.user;
import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JndiEntityManagerLookup;
import org.keycloak.connections.jpa.JpaKeycloakTransaction;
import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.storage.UserStorageProviderFactory;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.spi.PersistenceUnitTransactionType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ExampleUserStorageProviderFactory implements UserStorageProviderFactory<ExampleUserStorageProvider> {
protected String jndiName = "java:jboss/ExampleUserEntityManagerFactory";
EntityManagerFactory emf = null;
@Override
public ExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
EntityManager em = JndiEntityManagerLookup.getSessionEntityManager(session, jndiName);
EntityManager em = emf.createEntityManager();
session.getTransactionManager().enlist(new JpaKeycloakTransaction(em));
return new ExampleUserStorageProvider(em, model, session);
}
@ -50,11 +64,27 @@ public class ExampleUserStorageProviderFactory implements UserStorageProviderFac
@Override
public void postInit(KeycloakSessionFactory factory) {
//emf = Persistence.createEntityManagerFactory("user-storage-jpa-example");
emf = createEntityManagerFactory("user-storage-jpa-example");
//emf = Bootstrap.getEntityManagerFactoryBuilder()
}
public static EntityManagerFactory createEntityManagerFactory(String unitName) {
PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(ExampleUserStorageProviderFactory.class.getClassLoader()), PersistenceUnitTransactionType.RESOURCE_LOCAL);
List<ParsedPersistenceXmlDescriptor> persistenceUnits = parser.doResolve(new HashMap());
for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
if (persistenceUnit.getName().equals(unitName)) {
return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, new HashMap(), ExampleUserStorageProviderFactory.class.getClassLoader()).build();
}
}
throw new RuntimeException("Persistence unit '" + unitName + "' not found");
}
@Override
public void close() {
emf.close();
}
}

View file

@ -16,11 +16,13 @@
*/
package org.keycloak.examples.storage.user;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
@ -33,6 +35,7 @@ import java.util.Map;
* @version $Revision: 1 $
*/
public class UserAdapter extends AbstractUserAdapterFederatedStorage {
private static final Logger logger = Logger.getLogger(ExampleUserStorageProvider.class);
protected UserEntity entity;
protected String keycloakId;
@ -81,6 +84,13 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
public void setSingleAttribute(String name, String value) {
if (name.equals("phone")) {
entity.setPhone(value);
} else if (name.equals("password")) {
// ignore
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
// If we override getCredentialsDirectly/updateCredentialsDirectly
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
// If you don't like this workaround, you can query the database every time to validate the password
} else {
super.setSingleAttribute(name, value);
}
@ -90,6 +100,13 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
public void removeAttribute(String name) {
if (name.equals("phone")) {
entity.setPhone(null);
} else if (name.equals("password")) {
// ignore
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
// If we override getCredentialsDirectly/updateCredentialsDirectly
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
// If you don't like this workaround, you can query the database every time to validate the password
} else {
super.removeAttribute(name);
}
@ -99,6 +116,13 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
public void setAttribute(String name, List<String> values) {
if (name.equals("phone")) {
entity.setPhone(values.get(0));
} else if (name.equals("password")) {
// ignore
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
// If we override getCredentialsDirectly/updateCredentialsDirectly
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
// If you don't like this workaround, you can query the database every time to validate the password
} else {
super.setAttribute(name, values);
}
@ -108,6 +132,12 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
public String getFirstAttribute(String name) {
if (name.equals("phone")) {
return entity.getPhone();
} else if (name.equals("password")) {
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
// If we override getCredentialsDirectly/updateCredentialsDirectly
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
// If you don't like this workaround, you can query the database every time to validate the password
return entity.getPassword();
} else {
return super.getFirstAttribute(name);
}
@ -119,6 +149,12 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
MultivaluedHashMap<String, String> all = new MultivaluedHashMap<>();
all.putAll(attrs);
all.add("phone", entity.getPhone());
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
// If we override getCredentialsDirectly/updateCredentialsDirectly
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
// If you don't like this workaround, you can query the database every time to validate the password
all.add("password", entity.getPassword());
return all;
}

View file

@ -28,6 +28,10 @@ import javax.persistence.NamedQuery;
@NamedQueries({
@NamedQuery(name="getUserByUsername", query="select u from UserEntity u where u.username = :username"),
@NamedQuery(name="getUserByEmail", query="select u from UserEntity u where u.email = :email"),
@NamedQuery(name="getUserCount", query="select count(u) from UserEntity u"),
@NamedQuery(name="getAllUsers", query="select u from UserEntity u"),
@NamedQuery(name="searchForUser", query="select u from UserEntity u where " +
"( lower(u.username) like :search or u.email like :search ) order by u.username"),
})
@Entity
public class UserEntity {

View file

@ -4,13 +4,13 @@
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="primary">
<non-jta-data-source>java:jboss/datasources/ExampleUserDS</non-jta-data-source>
<persistence-unit name="user-storage-jpa-example" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<non-jta-data-source>java:jboss/datasources/ExampleDS</non-jta-data-source>
<class>org.keycloak.examples.storage.user.UserEntity</class>
<properties>
<property name="jboss.entity.manager.factory.jndi.name" value="java:jboss/ExampleUserEntityManagerFactory" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="false" />
</properties>

View file

@ -393,13 +393,13 @@ public class UserCacheSession implements CacheUserProvider {
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
return getDelegate().searchForUserByAttributes(attributes, realm);
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
return getDelegate().searchForUser(attributes, realm);
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
return getDelegate().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
return getDelegate().searchForUser(attributes, realm, firstResult, maxResults);
}
@Override

View file

@ -49,6 +49,7 @@ import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -547,7 +548,7 @@ public class JpaUserProvider implements UserProvider {
query.setMaxResults(maxResults);
}
List<UserEntity> results = query.getResultList();
List<UserModel> users = new ArrayList<UserModel>();
List<UserModel> users = new LinkedList<>();
for (UserEntity entity : results) users.add(new UserAdapter(session, realm, em, entity));
return users;
}
@ -564,7 +565,7 @@ public class JpaUserProvider implements UserProvider {
}
List<UserEntity> results = query.getResultList();
List<UserModel> users = new ArrayList<UserModel>();
List<UserModel> users = new LinkedList<>();
for (UserEntity user : results) {
users.add(new UserAdapter(session, realm, em, user));
}
@ -588,18 +589,18 @@ public class JpaUserProvider implements UserProvider {
query.setMaxResults(maxResults);
}
List<UserEntity> results = query.getResultList();
List<UserModel> users = new ArrayList<UserModel>();
List<UserModel> users = new LinkedList<>();
for (UserEntity entity : results) users.add(new UserAdapter(session, realm, em, entity));
return users;
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
return searchForUserByAttributes(attributes, realm, -1, -1);
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
return searchForUser(attributes, realm, -1, -1);
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
StringBuilder builder = new StringBuilder("select u from UserEntity u where u.realmId = :realmId");
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String attribute = null;

View file

@ -253,12 +253,12 @@ public class MongoUserProvider implements UserProvider {
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
return searchForUserByAttributes(attributes, realm, -1, -1);
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
return searchForUser(attributes, realm, -1, -1);
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
QueryBuilder queryBuilder = new QueryBuilder()
.and("realmId").is(realm.getId());

View file

@ -404,17 +404,17 @@ public class UserFederationManager implements UserProvider {
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
return searchForUserByAttributes(attributes, realm, 0, Integer.MAX_VALUE - 1);
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
}
@Override
public List<UserModel> searchForUserByAttributes(final Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public List<UserModel> searchForUser(final Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
federationLoad(realm, attributes);
return query(new PaginatedQuery() {
@Override
public List<UserModel> query(RealmModel realm, int first, int max) {
return session.userStorage().searchForUserByAttributes(attributes, realm, first, max);
return session.userStorage().searchForUser(attributes, realm, first, max);
}
}, realm, firstResult, maxResults);
}

View file

@ -45,7 +45,6 @@ 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;
import java.util.LinkedList;
@ -387,15 +386,15 @@ public class UserStorageManager implements UserProvider {
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
return searchForUserByAttributes(attributes, realm, 0, Integer.MAX_VALUE - 1);
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
return query((provider, first, max) -> {
if (provider instanceof UserQueryProvider) {
return ((UserQueryProvider)provider).searchForUserByAttributes(attributes, realm, first, max);
return ((UserQueryProvider)provider).searchForUser(attributes, realm, first, max);
}
return Collections.EMPTY_LIST;

View file

@ -29,24 +29,79 @@ import java.util.Map;
*/
public interface UserQueryProvider {
// Service account is included for counts
int getUsersCount(RealmModel realm);
List<UserModel> getUsers(RealmModel realm);
List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm);
List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults);
/**
* Search for users with username, email or first + last name that is like search string.
*
* If possible, implementations should treat the parameter values as partial match patterns i.e. in RDMBS terms use LIKE.
*
* This method is used by the admin console search box
*
* @param search
* @param realm
* @return
*/
List<UserModel> searchForUser(String search, RealmModel realm);
/**
* Search for users with username, email or first + last name that is like search string.
*
* If possible, implementations should treat the parameter values as partial match patterns i.e. in RDMBS terms use LIKE.
*
* This method is used by the admin console search box
*
* @param search
* @param realm
* @param firstResult
* @param maxResults
* @return
*/
List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults);
/**
* Search for user by parameter. Valid parameters are:
* "first" - first name
* "last" - last name
* "email" - email
* "username" - username
*
* If possible, implementations should treat the parameter values as partial match patterns i.e. in RDMBS terms use LIKE.
*
* This method is used by the REST API when querying users.
*
*
* @param params
* @param realm
* @return
*/
List<UserModel> searchForUser(Map<String, String> params, RealmModel realm);
/**
* Search for user by parameter. Valid parameters are:
* "first" - first name
* "last" - last name
* "email" - email
* "username" - username
*
* If possible, implementations should treat the parameter values as patterns i.e. in RDMBS terms use LIKE.
* This method is used by the REST API when querying users.
*
*
* @param params
* @param realm
* @param firstResult
* @param maxResults
* @return
*/
List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults);
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group);
List<UserModel> searchForUser(String search, RealmModel realm);
// Searching by UserModel.attribute (not property)
List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm);

View file

@ -46,7 +46,6 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -682,7 +681,7 @@ public class UsersResource {
if (username != null) {
attributes.put(UserModel.USERNAME, username);
}
userModels = session.users().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
userModels = session.users().searchForUser(attributes, realm, firstResult, maxResults);
} else {
userModels = session.users().getUsers(realm, firstResult, maxResults, false);
}

View file

@ -95,6 +95,7 @@ public class ServerInfoAdminResource {
}
private void setProviders(ServerInfoRepresentation info) {
info.setComponentTypes(new HashMap<>());
LinkedHashMap<String, SpiInfoRepresentation> spiReps = new LinkedHashMap<>();
List<Spi> spis = new LinkedList<>(session.getKeycloakSessionFactory().getSpis());
@ -115,7 +116,6 @@ public class ServerInfoAdminResource {
Map<String, ProviderRepresentation> providers = new HashMap<>();
if (providerIds != null) {
info.setComponentTypes(new HashMap<>());
for (String name : providerIds) {
ProviderRepresentation provider = new ProviderRepresentation();
ProviderFactory<?> pi = session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name);

View file

@ -0,0 +1,46 @@
/*
* 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.testsuite.federation.storage;
import org.keycloak.hash.PasswordHashProvider;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PlainTextPasswordProvider implements PasswordHashProvider {
@Override
public UserCredentialValueModel encode(String rawPassword, int iterations) {
UserCredentialValueModel model = new UserCredentialValueModel();
model.setType(UserCredentialModel.PASSWORD);
model.setValue(rawPassword);
model.setAlgorithm("text");
return model;
}
@Override
public boolean verify(String rawPassword, UserCredentialValueModel credential) {
return rawPassword.equals(credential.getValue());
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.testsuite.federation.storage;
import org.keycloak.Config;
import org.keycloak.hash.PasswordHashProvider;
import org.keycloak.hash.PasswordHashProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PlainTextPasswordProviderFactory implements PasswordHashProviderFactory {
@Override
public PasswordHashProvider create(KeycloakSession session) {
return new PlainTextPasswordProvider();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "text";
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
@ -30,6 +31,7 @@ import org.keycloak.storage.user.UserCredentialValidatorProvider;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@ -38,7 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserMapStorage implements UserLookupProvider, UserStorageProvider, UserCredentialValidatorProvider, UserRegistrationProvider {
public class UserMapStorage implements UserLookupProvider, UserStorageProvider, UserRegistrationProvider {
protected Map<String, String> userPasswords;
protected ComponentModel model;
@ -79,11 +81,33 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
@Override
public void updateCredential(UserCredentialModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
userPasswords.put(username, cred.getValue());
userPasswords.put(getUsername(), cred.getValue());
} else {
super.updateCredential(cred);
}
}
@Override
public List<UserCredentialValueModel> getCredentialsDirectly() {
UserCredentialValueModel pw = new UserCredentialValueModel();
pw.setId(getId());
pw.setType(UserCredentialModel.PASSWORD);
pw.setAlgorithm("text");
pw.setValue(userPasswords.get(getUsername()));
List<UserCredentialValueModel> creds = new LinkedList<>();
creds.addAll(super.getCredentialsDirectly());
creds.add(pw);
return creds;
}
@Override
public void updateCredentialDirectly(UserCredentialValueModel cred) {
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
//userPasswords.put(getUsername(), cred.getValue());
} else {
super.updateCredentialDirectly(cred);
}
}
};
}
@ -130,19 +154,6 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
}
@Override
public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input) {
for (UserCredentialModel cred : input) {
if (!cred.getType().equals(UserCredentialModel.PASSWORD)) return false;
String password = (String)userPasswords.get(user.getUsername());
if (password == null) return false;
if (!password.equals(cred.getValue())) return false;
}
return true;
}
@Override
public void close() {
closings.incrementAndGet();

View file

@ -141,7 +141,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
return Collections.EMPTY_LIST;
}
@ -178,7 +178,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
}
@Override
public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public List<UserModel> searchForUser(Map<String, String> 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;

View file

@ -108,7 +108,6 @@ public class UserStorageTest {
Assert.assertEquals("Invalid username or password.", loginPage.getError());
}
@Test
public void testLoginSuccess() {
loginSuccessAndLogout("tbrady", "goat");
@ -258,12 +257,14 @@ public class UserStorageTest {
user.updateCredential(UserCredentialModel.password("password"));
keycloakRule.stopSession(session, true);
loginSuccessAndLogout("memuser", "password");
loginSuccessAndLogout("memuser", "password");
loginSuccessAndLogout("memuser", "password");
session = keycloakRule.startSession();
realm = session.realms().getRealmByName("test");
user = session.users().getUserByUsername("memuser", realm);
Assert.assertEquals(memoryProvider.getId(), StorageId.resolveProviderId(user));
Assert.assertEquals(0, user.getCredentialsDirectly().size());
Assert.assertEquals(1, user.getCredentialsDirectly().size());
session.users().removeUser(realm, user);
Assert.assertNull(session.users().getUserByUsername("memuser", realm));
keycloakRule.stopSession(session, true);

View file

@ -66,20 +66,20 @@ public class UserModelTest extends AbstractModelTest {
Map<String, String> attributes = new HashMap<String, String>();
attributes.put(UserModel.LAST_NAME, "last-name");
List<UserModel> search = session.users().searchForUserByAttributes(attributes, realm);
List<UserModel> search = session.users().searchForUser(attributes, realm);
Assert.assertEquals(search.size(), 1);
Assert.assertEquals(search.get(0).getUsername(), "user");
attributes.clear();
attributes.put(UserModel.EMAIL, "email");
search = session.users().searchForUserByAttributes(attributes, realm);
search = session.users().searchForUser(attributes, realm);
Assert.assertEquals(search.size(), 1);
Assert.assertEquals(search.get(0).getUsername(), "user");
attributes.clear();
attributes.put(UserModel.LAST_NAME, "last-name");
attributes.put(UserModel.EMAIL, "email");
search = session.users().searchForUserByAttributes(attributes, realm);
search = session.users().searchForUser(attributes, realm);
Assert.assertEquals(search.size(), 1);
Assert.assertEquals(search.get(0).getUsername(), "user");
}

View file

@ -0,0 +1 @@
org.keycloak.testsuite.federation.storage.PlainTextPasswordProviderFactory

View file

@ -597,6 +597,7 @@ client-template.description.tooltip=Description of the client template
client-template.protocol.tooltip=Which SSO protocol configuration is being supplied by this client template
add-user-federation-provider=Add user federation provider
add-user-storage-provider=Add user storage provider
required-settings=Required Settings
provider-id=Provider ID
console-display-name=Console Display Name

View file

@ -1350,6 +1350,44 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'UserStorageCtrl'
})
.when('/create/user-storage/:realm/providers/:provider', {
templateUrl : resourceUrl + '/partials/user-storage-generic.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
instance : function() {
return {
};
},
providerId : function($route) {
return $route.current.params.provider;
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'GenericUserStorageCtrl'
})
.when('/realms/:realm/user-storage/providers/:provider/:componentId', {
templateUrl : resourceUrl + '/partials/user-storage-generic.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
instance : function(ComponentLoader) {
return ComponentLoader();
},
providerId : function($route) {
return $route.current.params.provider;
},
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'GenericUserStorageCtrl'
})
.when('/realms/:realm/user-federation', {
templateUrl : resourceUrl + '/partials/user-federation.html',
resolve : {
@ -2344,6 +2382,89 @@ module.directive('kcProviderConfig', function ($modal) {
}
});
module.controller('ComponentRoleSelectorModalCtrl', function($scope, realm, config, configName, RealmRoles, Client, ClientRole, $modalInstance) {
$scope.selectedRealmRole = {
role: undefined
};
$scope.selectedClientRole = {
role: undefined
};
$scope.client = {
selected: undefined
};
$scope.selectRealmRole = function() {
config[configName][0] = $scope.selectedRealmRole.role.name;
$modalInstance.close();
}
$scope.selectClientRole = function() {
config[configName][0] = $scope.client.selected.clientId + "." + $scope.selectedClientRole.role.name;
$modalInstance.close();
}
$scope.cancel = function() {
$modalInstance.dismiss();
}
$scope.changeClient = function() {
if ($scope.client.selected) {
ClientRole.query({realm: realm.realm, client: $scope.client.selected.id}, function (data) {
$scope.clientRoles = data;
});
} else {
console.log('selected client was null');
$scope.clientRoles = null;
}
}
RealmRoles.query({realm: realm.realm}, function(data) {
$scope.realmRoles = data;
})
Client.query({realm: realm.realm}, function(data) {
$scope.clients = data;
if (data.length > 0) {
$scope.client.selected = data[0];
$scope.changeClient();
}
})
});
module.controller('ComponentConfigCtrl', function ($modal, $scope) {
$scope.openRoleSelector = function (configName, config) {
$modal.open({
templateUrl: resourceUrl + '/partials/modal/component-role-selector.html',
controller: 'ComponentRoleSelectorModalCtrl',
resolve: {
realm: function () {
return $scope.realm;
},
config: function () {
return config;
},
configName: function () {
return configName;
}
}
})
}
});
module.directive('kcComponentConfig', function ($modal) {
return {
scope: {
config: '=',
properties: '=',
realm: '=',
clients: '=',
configName: '='
},
restrict: 'E',
replace: true,
controller: 'ComponentConfigCtrl',
templateUrl: resourceUrl + '/templates/kc-component-config.html'
}
});
/*
* Used to select the element (invoke $(elem).select()) on specified action list.
* Usages kc-select-action="click mouseover"

View file

@ -620,6 +620,123 @@ module.controller('UserStorageCtrl', function($scope, $location, $route, realm,
};
});
module.controller('GenericUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm, serverInfo, instance, providerId, Components) {
console.log('GenericUserFederationCtrl');
console.log('providerId: ' + providerId);
$scope.create = !instance.providerId;
console.log('create: ' + $scope.create);
var providers = serverInfo.componentTypes['org.keycloak.storage.UserStorageProvider'];
console.log('providers length ' + providers.length);
var providerFactory = null;
for (var i = 0; i < providers.length; i++) {
var p = providers[i];
console.log('provider: ' + p.id);
if (p.id == providerId) {
$scope.providerFactory = p;
providerFactory = p;
break;
}
}
$scope.provider = instance;
console.log("providerFactory: " + providerFactory.id);
function initUserStorageSettings() {
if ($scope.create) {
instance.name = providerFactory.id;
instance.providerId = providerFactory.id;
instance.providerType = 'org.keycloak.storage.UserStorageProvider';
instance.parentId = realm.id;
instance.config = {
};
instance.config['priority'] = ["0"];
if (providerFactory.properties) {
for (var i = 0; i < providerFactory.properties.length; i++) {
var configProperty = providerFactory.properties[i];
if (configProperty.defaultValue) {
instance.config[configProperty.name] = [configProperty.defaultValue];
} else {
instance.config[configProperty.name] = [''];
}
}
}
} else {
/*
console.log('Manage instance');
console.log(instance.name);
console.log(instance.providerId);
console.log(instance.providerType);
console.log(instance.parentId);
for (var k in instance.config) {
console.log('config[' + k + "] =");
}
*/
}
$scope.changed = false;
}
initUserStorageSettings();
$scope.instance = angular.copy(instance);
$scope.realm = realm;
$scope.$watch('instance', function() {
if (!angular.equals($scope.instance, instance)) {
$scope.changed = true;
}
}, true);
$scope.save = function() {
$scope.changed = false;
if ($scope.create) {
Components.save({realm: realm.realm}, $scope.instance, function (data, headers) {
var l = headers().location;
var id = l.substring(l.lastIndexOf("/") + 1);
$location.url("/realms/" + realm.realm + "/user-storage/providers/" + $scope.instance.providerId + "/" + id);
Notifications.success("The provider has been created.");
}, function (errorResponse) {
if (errorResponse.data && errorResponse.data['error_description']) {
Notifications.error(errorResponse.data['error_description']);
}
});
} else {
Components.update({realm: realm.realm,
componentId: instance.id
},
$scope.instance, function () {
$route.reload();
Notifications.success("The provider has been updated.");
}, function (errorResponse) {
if (errorResponse.data && errorResponse.data['error_description']) {
Notifications.error(errorResponse.data['error_description']);
}
});
}
};
$scope.reset = function() {
initUserStorageSettings();
$scope.instance = angular.copy(instance);
};
$scope.cancel = function() {
if ($scope.create) {
$location.url("/realms/" + realm.realm + "/user-storage");
} else {
$route.reload();
}
};
});
module.controller('UserFederationCtrl', function($scope, $location, $route, realm, UserFederationProviders, UserFederationInstances, Notifications, Dialog) {
console.log('UserFederationCtrl ++++****');
$scope.realm = realm;

View file

@ -126,6 +126,15 @@ module.factory('UserLoader', function(Loader, User, $route, $q) {
});
});
module.factory('ComponentLoader', function(Loader, Components, $route, $q) {
return Loader.get(Components, function() {
return {
realm : $route.current.params.realm,
componentId: $route.current.params.componentId
}
});
});
module.factory('UserFederationInstanceLoader', function(Loader, UserFederationInstances, $route, $q) {
return Loader.get(UserFederationInstances, function() {
return {

View file

@ -0,0 +1,54 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<ol class="breadcrumb">
<li><a href="#/realms/{{realm.realm}}/user-storage">{{:: 'user-storage' | translate}}</a></li>
<li data-ng-hide="create">{{instance.name|capitalize}}</li>
<li data-ng-show="create">{{:: 'add-user-storage-provider' | translate}}</li>
</ol>
<kc-tabs-user-storage></kc-tabs-user-storage>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset>
<legend><span class="text">{{:: 'required-settings' | translate}}</span></legend>
<div class="form-group clearfix" data-ng-show="!create">
<label class="col-md-2 control-label" for="providerId">{{:: 'provider-id' | translate}} </label>
<div class="col-md-6">
<input class="form-control" id="providerId" type="text" ng-model="instance.id" readonly>
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="consoleDisplayName">{{:: 'console-display-name' | translate}} </label>
<div class="col-md-6">
<input class="form-control" id="consoleDisplayName" type="text" ng-model="instance.name" placeholder="{{:: 'defaults-to-id' | translate}}">
</div>
<kc-tooltip>{{:: 'console-display-name.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="priority">{{:: 'priority' | translate}} </label>
<div class="col-md-6">
<input class="form-control" id="priority" type="text" ng-model="instance.config['priority'][0]">
</div>
<kc-tooltip>{{:: 'priority.tooltip' | translate}}</kc-tooltip>
</div>
<kc-component-config realm="realm" config="instance.config" properties="providerFactory.properties"></kc-component-config>
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
<button kc-save>{{:: 'save' | translate}}</button>
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
</div>
</div>
<div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -1,6 +1,6 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>
<span>{{:: 'user-federation' | translate}}</span>
<span>{{:: 'user-storage' | translate}}</span>
</h1>
<table class="table table-striped table-bordered">

View file

@ -0,0 +1,43 @@
<div>
<div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl">
<label class="col-md-2 control-label">{{:: option.label | translate}}</label>
<div class="col-md-6" data-ng-hide="option.type == 'boolean' || option.type == 'List' || option.type == 'Role' || option.type == 'ClientList' || option.type == 'Password' || option.type=='Script'">
<input class="form-control" type="text" data-ng-model="config[ option.name ][0]" >
</div>
<div class="col-md-6" data-ng-show="option.type == 'Password'">
<input class="form-control" type="password" data-ng-model="config[ option.name ][0]" >
</div>
<div class="col-md-6" data-ng-show="option.type == 'boolean'">
<input ng-model="config[ option.name ][0]" value="'true'" name="option.name" id="option.name" onoffswitchstring on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<div class="col-md-6" data-ng-show="option.type == 'List'">
<select ng-model="config[ option.name ][0]" ng-options="data for data in option.defaultValue">
<option value="" selected> {{:: 'selectOne' | translate}} </option>
</select>
</div>
<div class="col-md-6" data-ng-show="option.type == 'Role'">
<div class="row">
<div class="col-md-8">
<input class="form-control" type="text" data-ng-model="config[ option.name ][0]" >
</div>
<div class="col-md-2">
<button type="submit" data-ng-click="openRoleSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'selectRole.tooltip' | translate}}">{{:: 'selectRole.label' | translate}}</button>
</div>
</div>
</div>
<div class="col-md-4" data-ng-show="option.type == 'ClientList'">
<select ng-model="config[ option.name ][0]" ng-options="client.clientId as client.clientId for client in clients">
<option value="" selected> {{:: 'selectOne' | translate}} </option>
</select>
</div>
<div class="col-md-6" data-ng-show="option.type == 'Script'">
<div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
{{config[option.name]}}
</div>
</div>
<kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
</div>
</div>

View file

@ -34,7 +34,7 @@
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || path[2] == 'default-roles' || (path[1] == 'role' && path[3] != 'clients')) && 'active'"><a href="#/realms/{{realm.realm}}/roles"><i class="fa fa-tasks"></i> {{:: 'roles' | translate}}</a></li>
<li data-ng-show="access.viewIdentityProviders" data-ng-class="(path[2] == 'identity-provider-settings' || path[2] == 'identity-provider-mappers') && 'active'"><a href="#/realms/{{realm.realm}}/identity-provider-settings"><i class="fa fa-exchange"></i> {{:: 'identity-providers' | translate}}</a></li>
<li data-ng-show="access.viewUsers" data-ng-class="(path[1] == 'user-federation' || path[2] == 'user-federation') && 'active'"><a href="#/realms/{{realm.realm}}/user-federation"><i class="fa fa-database"></i> {{:: 'user-federation' | translate}}</a></li>
<!-- <li data-ng-show="access.viewUsers" data-ng-class="(path[1] == 'user-storage' || path[2] == 'user-storage') && 'active'"><a href="#/realms/{{realm.realm}}/user-storage"><i class="fa fa-database"></i> {{:: 'user-storage' | translate}}</a></li> -->
<li data-ng-show="access.viewUsers" data-ng-class="(path[1] == 'user-storage' || path[2] == 'user-storage') && 'active'"><a href="#/realms/{{realm.realm}}/user-storage"><i class="fa fa-database"></i> {{:: 'user-storage' | translate}}</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[1] == 'authentication' || path[2] == 'authentication') && 'active'"><a href="#/realms/{{realm.realm}}/authentication/flows"><i class="fa fa-lock"></i> {{:: 'authentication' | translate}}</a></li>
</ul>
</div>

View file

@ -0,0 +1,11 @@
<div data-ng-controller="UserStorageTabCtrl">
<h1 data-ng-hide="create">
{{instance.name|capitalize}}
<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserStorage()"></i>
</h1>
<h1 data-ng-show="create">{{:: 'add-user-storage-provider' | translate}}</h1>
<ul class="nav nav-tabs" data-ng-hide="create">
<li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/user-storage/providers/{{instance.providerName}}/{{instance.id}}">{{:: 'settings' | translate}}</a></li>
</ul>
</div>