From b51661b790eab3c7f9f2abf533b1ce6dad8c371e Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 20 Jul 2015 17:11:04 +0200 Subject: [PATCH 01/13] KEYCLOAK-1542 - added system and memory info into "System Info" page --- .../admin/resources/partials/server-info.html | 75 +++++++- .../models/KeycloakSessionFactory.java | 2 + .../DefaultKeycloakSessionFactory.java | 12 ++ .../admin/ServerInfoAdminResource.java | 173 ++++++++++++++++-- 4 files changed, 249 insertions(+), 13 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 34c9a845d3a..5590f9576e1 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -8,10 +8,81 @@ Server Time - {{serverInfo.serverTime}} (update) + {{serverInfo.serverTime}} (update) + + + Server Uptime + {{serverInfo.serverUptime}} + + + Current Working Directory + {{serverInfo.systemInfo.userDir}} + + + Java Version + {{serverInfo.systemInfo.javaVersion}} + + + Java Vendor + {{serverInfo.systemInfo.javaVendor}} + + + Java Runtime + {{serverInfo.systemInfo.javaRuntime}} + + + Java VM + {{serverInfo.systemInfo.javaVm}} + + + Java VM Version + {{serverInfo.systemInfo.javaVmVersion}} + + + Java Home + {{serverInfo.systemInfo.javaHome}} + + + User Name + {{serverInfo.systemInfo.userName}} + + + User Timezone + {{serverInfo.systemInfo.userTimezone}} + + + User Locale + {{serverInfo.systemInfo.userLocale}} + + + System Encoding + {{serverInfo.systemInfo.fileEncoding}} + + + Operating System + {{serverInfo.systemInfo.osName}} {{serverInfo.systemInfo.osVersion}} + + + OS Architecture + {{serverInfo.systemInfo.osArchitecture}} - + +

Java VM Memory Statistics

+ + + + + + + + + + + + + +
Total Memory{{serverInfo.memoryInfo.totalFormated}}
Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)
Used Memory{{serverInfo.memoryInfo.usedFormated}}
Providers diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java index cbb3da22d5f..82665508c25 100755 --- a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java @@ -18,6 +18,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager { ProviderFactory getProviderFactory(Class clazz, String id); List getProviderFactories(Class clazz); + + long getServerStartupTimestamp(); void close(); } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index e312aa01225..c5be21be10e 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -28,6 +28,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { private Map, Map> factoriesMap = new HashMap, Map>(); protected CopyOnWriteArrayList listeners = new CopyOnWriteArrayList(); + protected long serverStartupTimestamp; + @Override public void register(ProviderEventListener listener) { listeners.add(listener); @@ -46,6 +48,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { } public void init() { + serverStartupTimestamp = System.currentTimeMillis(); + ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers")); for (Spi spi : ServiceLoader.load(Spi.class, getClass().getClassLoader())) { @@ -148,4 +152,12 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { return factory.getClass().getPackage().getName().startsWith("org.keycloak"); } + /** + * @return timestamp of Keycloak server startup + */ + @Override + public long getServerStartupTimestamp() { + return serverStartupTimestamp; + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index 4f1c22575b0..c600b92e4b4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,5 +1,18 @@ package org.keycloak.services.resources.admin; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; + import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; @@ -24,17 +37,6 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.social.SocialIdentityProvider; -import javax.ws.rs.GET; -import javax.ws.rs.core.Context; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; - /** * @author Stian Thorgersen */ @@ -55,6 +57,7 @@ public class ServerInfoAdminResource { ServerInfoRepresentation info = new ServerInfoRepresentation(); info.version = Version.VERSION; info.serverTime = new Date().toString(); + info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); setSocialProviders(info); setIdentityProviders(info); setThemes(info); @@ -187,12 +190,115 @@ public class ServerInfoAdminResource { info.clientImporters.add(data); } } + + public static class MemoryInfo{ + + public long getTotal(){ + return Runtime.getRuntime().maxMemory(); + } + + public String getTotalFormated(){ + return formatMemory(getTotal()); + } + + public long getFree(){ + return getTotal() - getUsed(); + } + public String getFreeFormated(){ + return formatMemory(getFree()); + } + + public long getUsed(){ + return Runtime.getRuntime().totalMemory(); + } + + public String getUsedFormated(){ + return formatMemory(getUsed()); + } + + public long getFreePercentage(){ + return getFree()*100/getTotal(); + } + + private String formatMemory(long bytes){ + if(bytes > 1024L*1024L){ + return bytes/(1024L *1024L) + " MB"; + } else if(bytes > 1024L){ + return bytes/(1024L) + " kB"; + } else { + return bytes + " B"; + } + } + + } + + public static class SystemInfo { + public String getJavaVersion(){ + return System.getProperty("java.version"); + } + + public String getJavaVendor(){ + return System.getProperty("java.vendor"); + } + + public String getJavaVm(){ + return System.getProperty("java.vm.name"); + } + + public String getJavaVmVersion(){ + return System.getProperty("java.vm.version"); + } + + public String getJavaRuntime(){ + return System.getProperty("java.runtime.name"); + } + + public String getJavaHome(){ + return System.getProperty("java.home"); + } + + public String getOsName(){ + return System.getProperty("os.name"); + } + + public String getOsArchitecture(){ + return System.getProperty("os.arch"); + } + + public String getOsVersion(){ + return System.getProperty("os.version"); + } + + public String getFileEncoding(){ + return System.getProperty("file.encoding"); + } + + public String getUserName(){ + return System.getProperty("user.name"); + } + + public String getUserDir(){ + return System.getProperty("user.dir"); + } + + public String getUserTimezone(){ + return System.getProperty("user.timezone"); + } + + public String getUserLocale(){ + return (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + } + + } + public static class ServerInfoRepresentation { private String version; private String serverTime; + + private long serverStartupTime; private Map> themes; @@ -211,10 +317,55 @@ public class ServerInfoAdminResource { public ServerInfoRepresentation() { } + + public SystemInfo getSystemInfo(){ + return new SystemInfo(); + } + + public MemoryInfo getMemoryInfo(){ + return new MemoryInfo(); + } public String getServerTime() { return serverTime; } + + /** + * @return server startup time formatted + */ + public String getServerStartupTime() { + return (new Date(serverStartupTime)).toString(); + } + + /** + * @return server uptime in millis + */ + public long getServerUptimeMillis(){ + return System.currentTimeMillis() - serverStartupTime; + } + + /** + * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" + */ + public String getServerUptime(){ + long diffInSeconds = getServerUptimeMillis()/1000; + long diff[] = new long[] { 0, 0, 0, 0 }; + /* sec */diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); + /* min */diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; + /* hours */diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; + /* days */diff[0] = (diffInSeconds = (diffInSeconds / 24)); + + return String.format( + "%d day%s, %d hour%s, %d minute%s, %d second%s", + diff[0], + diff[0] != 1 ? "s" : "", + diff[1], + diff[1] != 1 ? "s" : "", + diff[2], + diff[2] != 1 ? "s" : "", + diff[3], + diff[3] != 1 ? "s" : ""); + } public String getVersion() { return version; From 84e40cbc2c108aca6503563127b886c98b9fe10a Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Tue, 21 Jul 2015 16:09:47 +0200 Subject: [PATCH 02/13] KEYCLOAK-1542 - Server Info page extended by info about DB and MongoDB. Functional test for /serverinfo REST endpoint added. --- .../DefaultJpaConnectionProviderFactory.java | 341 ++++++++++------- .../jpa/JpaConnectionProviderFactory.java | 5 +- ...DefaultMongoConnectionFactoryProvider.java | 356 ++++++++++-------- .../mongo/MongoConnectionProviderFactory.java | 4 +- .../admin/resources/partials/server-info.html | 130 +++++-- .../provider/MonitorableProviderFactory.java | 18 + .../provider/ProviderOperationalInfo.java | 22 ++ services/pom.xml | 15 + .../admin/ServerInfoAdminResource.java | 154 ++++++-- .../testsuite/admin/AdminAPITest.java | 63 +++- 10 files changed, 720 insertions(+), 388 deletions(-) create mode 100644 model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java create mode 100644 model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 61bbafa6bf9..36f3c09cce7 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -1,189 +1,242 @@ package org.keycloak.connections.jpa; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import javax.naming.InitialContext; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; + import org.hibernate.ejb.AvailableSettings; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; - -import javax.naming.InitialContext; -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.Persistence; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; +import org.keycloak.provider.ProviderOperationalInfo; /** * @author Stian Thorgersen */ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory { - private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); + private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); - private volatile EntityManagerFactory emf; + private volatile EntityManagerFactory emf; - private Config.Scope config; + private Config.Scope config; - @Override - public JpaConnectionProvider create(KeycloakSession session) { - lazyInit(session); + private DatabaseInfo databaseInfo; - EntityManager em = emf.createEntityManager(); - em = PersistenceExceptionConverter.create(em); - session.getTransaction().enlist(new JpaKeycloakTransaction(em)); - return new DefaultJpaConnectionProvider(em); - } + @Override + public JpaConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public void close() { - if (emf != null) { - emf.close(); - } - } + EntityManager em = emf.createEntityManager(); + em = PersistenceExceptionConverter.create(em); + session.getTransaction().enlist(new JpaKeycloakTransaction(em)); + return new DefaultJpaConnectionProvider(em); + } - @Override - public String getId() { - return "default"; - } + @Override + public void close() { + if (emf != null) { + emf.close(); + } + } - @Override - public void init(Config.Scope config) { - this.config = config; - } + @Override + public String getId() { + return "default"; + } - @Override - public void postInit(KeycloakSessionFactory factory) { + @Override + public void init(Config.Scope config) { + this.config = config; + } - } + @Override + public void postInit(KeycloakSessionFactory factory) { - private void lazyInit(KeycloakSession session) { - if (emf == null) { - synchronized (this) { - if (emf == null) { - logger.debug("Initializing JPA connections"); + } - Connection connection = null; + private void lazyInit(KeycloakSession session) { + if (emf == null) { + synchronized (this) { + if (emf == null) { + logger.debug("Initializing JPA connections"); - String databaseSchema = config.get("databaseSchema"); + Connection connection = null; - Map properties = new HashMap(); + String databaseSchema = config.get("databaseSchema"); - String unitName = "keycloak-default"; + Map properties = new HashMap(); - String dataSource = config.get("dataSource"); - if (dataSource != null) { - if (config.getBoolean("jta", false)) { - properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); - } else { - properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); - } - } else { - properties.put(AvailableSettings.JDBC_URL, config.get("url")); - properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); + String unitName = "keycloak-default"; - String user = config.get("user"); - if (user != null) { - properties.put(AvailableSettings.JDBC_USER, user); - } - String password = config.get("password"); - if (password != null) { - properties.put(AvailableSettings.JDBC_PASSWORD, password); - } - } + String dataSource = config.get("dataSource"); + if (dataSource != null) { + if (config.getBoolean("jta", false)) { + properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); + } else { + properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); + } + } else { + properties.put(AvailableSettings.JDBC_URL, config.get("url")); + properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); - String driverDialect = config.get("driverDialect"); - if (driverDialect != null && driverDialect.length() > 0) { - properties.put("hibernate.dialect", driverDialect); - } + String user = config.get("user"); + if (user != null) { + properties.put(AvailableSettings.JDBC_USER, user); + } + String password = config.get("password"); + if (password != null) { + properties.put(AvailableSettings.JDBC_PASSWORD, password); + } + } - String schema = config.get("schema"); - if (schema != null) { - properties.put("hibernate.default_schema", schema); - } + String driverDialect = config.get("driverDialect"); + if (driverDialect != null && driverDialect.length() > 0) { + properties.put("hibernate.dialect", driverDialect); + } - if (databaseSchema != null) { - if (databaseSchema.equals("development-update")) { - properties.put("hibernate.hbm2ddl.auto", "update"); - databaseSchema = null; - } else if (databaseSchema.equals("development-validate")) { - properties.put("hibernate.hbm2ddl.auto", "validate"); - databaseSchema = null; - } - } + String schema = config.get("schema"); + if (schema != null) { + properties.put("hibernate.default_schema", schema); + } - properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); - properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); + if (databaseSchema != null) { + if (databaseSchema.equals("development-update")) { + properties.put("hibernate.hbm2ddl.auto", "update"); + databaseSchema = null; + } else if (databaseSchema.equals("development-validate")) { + properties.put("hibernate.hbm2ddl.auto", "validate"); + databaseSchema = null; + } + } - if (databaseSchema != null) { - logger.trace("Updating database"); + properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); + properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); - JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); - if (updater == null) { - throw new RuntimeException("Can't update database: JPA updater provider not found"); - } + connection = getConnection(); + prepareDatabaseInfo(connection); - connection = getConnection(); + if (databaseSchema != null) { + logger.trace("Updating database"); - if (databaseSchema.equals("update")) { - String currentVersion = null; - try { - ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); - if (resultSet.next()) { - currentVersion = resultSet.getString(1); - } - } catch (SQLException e) { - } + JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); + if (updater == null) { + throw new RuntimeException("Can't update database: JPA updater provider not found"); + } - if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { - updater.update(session, connection, schema); - } else { - logger.debug("Database is up to date"); - } - } else if (databaseSchema.equals("validate")) { - updater.validate(connection, schema); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } + if (databaseSchema.equals("update")) { + String currentVersion = null; + try { + ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); + if (resultSet.next()) { + currentVersion = resultSet.getString(1); + } + } catch (SQLException e) { + } - logger.trace("Database update completed"); - } + if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { + updater.update(session, connection, schema); + } else { + logger.debug("Database is up to date"); + } + } else if (databaseSchema.equals("validate")) { + updater.validate(connection, schema); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } - logger.trace("Creating EntityManagerFactory"); - emf = Persistence.createEntityManagerFactory(unitName, properties); - logger.trace("EntityManagerFactory created"); + logger.trace("Database update completed"); + } - // Close after creating EntityManagerFactory to prevent in-mem databases from closing - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - logger.warn(e); - } - } - } - } - } - } + logger.trace("Creating EntityManagerFactory"); + emf = Persistence.createEntityManagerFactory(unitName, properties); + logger.trace("EntityManagerFactory created"); - private Connection getConnection() { - try { - String dataSourceLookup = config.get("dataSource"); - if (dataSourceLookup != null) { - DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); - return dataSource.getConnection(); - } else { - Class.forName(config.get("driver")); - return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); - } - } catch (Exception e) { - throw new RuntimeException("Failed to connect to database", e); - } - } + // Close after creating EntityManagerFactory to prevent in-mem databases from closing + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + logger.warn(e); + } + } + } + } + } + } + + protected void prepareDatabaseInfo(Connection connection) { + try { + databaseInfo = new DatabaseInfo(); + DatabaseMetaData md = connection.getMetaData(); + databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); + databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); + databaseInfo.databaseUser = md.getUserName(); + databaseInfo.jdbcUrl = md.getURL(); + } catch (SQLException e) { + logger.warn("Unable to get database info due " + e.getMessage()); + } + } + + private Connection getConnection() { + try { + String dataSourceLookup = config.get("dataSource"); + if (dataSourceLookup != null) { + DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); + return dataSource.getConnection(); + } else { + Class.forName(config.get("driver")); + return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); + } + } catch (Exception e) { + throw new RuntimeException("Failed to connect to database", e); + } + } + + @Override + public DatabaseInfo getOperationalInfo() { + return databaseInfo; + } + + public static class DatabaseInfo implements ProviderOperationalInfo { + protected String jdbcUrl; + protected String databaseUser; + protected String databaseProduct; + protected String databaseDriver; + + public String getJdbcUrl() { + return jdbcUrl; + } + + public String getDatabaseDriver() { + return databaseDriver; + } + + public String getDatabaseUser() { + return databaseUser; + } + + public String getDatabaseProduct() { + return databaseProduct; + } + + @Override + public boolean isOk() { + // TODO KEYCLOAK-1578 - implement operational monitoring of JPA DB connection + return true; + } + } } diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java index 1cf4a5f2026..ab2b119c29c 100644 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java @@ -1,9 +1,10 @@ package org.keycloak.connections.jpa; -import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.MonitorableProviderFactory; /** * @author Stian Thorgersen */ -public interface JpaConnectionProviderFactory extends ProviderFactory { +public interface JpaConnectionProviderFactory extends MonitorableProviderFactory { + } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 36be680a668..c8d1cbaed4a 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -1,10 +1,11 @@ package org.keycloak.connections.mongo; -import com.mongodb.DB; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; +import java.lang.reflect.Method; +import java.net.UnknownHostException; +import java.util.Collections; + +import javax.net.ssl.SSLSocketFactory; + import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.connections.mongo.api.MongoStore; @@ -13,198 +14,225 @@ import org.keycloak.connections.mongo.impl.context.TransactionMongoStoreInvocati import org.keycloak.connections.mongo.updater.MongoUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderOperationalInfo; -import javax.net.ssl.SSLSocketFactory; -import java.lang.reflect.Method; -import java.net.UnknownHostException; -import java.util.Collections; +import com.mongodb.DB; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; /** * @author Stian Thorgersen */ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { - // TODO Make configurable - private String[] entities = new String[]{ - "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", - "org.keycloak.models.entities.IdentityProviderEntity", - "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", - "org.keycloak.models.entities.RequiredCredentialEntity", - "org.keycloak.models.entities.CredentialEntity", - "org.keycloak.models.entities.FederatedIdentityEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", - "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", - "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", - "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", - "org.keycloak.models.entities.UserFederationProviderEntity", - "org.keycloak.models.entities.UserFederationMapperEntity", - "org.keycloak.models.entities.ProtocolMapperEntity", - "org.keycloak.models.entities.IdentityProviderMapperEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", - "org.keycloak.models.entities.AuthenticationExecutionEntity", - "org.keycloak.models.entities.AuthenticationFlowEntity", - "org.keycloak.models.entities.AuthenticatorConfigEntity", - "org.keycloak.models.entities.RequiredActionProviderEntity", - }; + // TODO Make configurable + private String[] entities = new String[] { "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", + "org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.CredentialEntity", + "org.keycloak.models.entities.FederatedIdentityEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", + "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", "org.keycloak.models.entities.UserFederationProviderEntity", + "org.keycloak.models.entities.UserFederationMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity", "org.keycloak.models.entities.IdentityProviderMapperEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.entities.AuthenticationExecutionEntity", + "org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.RequiredActionProviderEntity", }; - private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); + private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); - private volatile MongoClient client; + private volatile MongoClient client; - private MongoStore mongoStore; - private DB db; - protected Config.Scope config; + private MongoStore mongoStore; + private DB db; + protected Config.Scope config; - @Override - public MongoConnectionProvider create(KeycloakSession session) { - lazyInit(session); + private MongoDbInfo mongoDbInfo; - TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); - session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); - return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); - } + @Override + public MongoConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public void init(Config.Scope config) { - this.config = config; - } + TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); + session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); + return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); + } - @Override - public void postInit(KeycloakSessionFactory factory) { + @Override + public void init(Config.Scope config) { + this.config = config; + } - } + @Override + public void postInit(KeycloakSessionFactory factory) { + } - private void lazyInit(KeycloakSession session) { - if (client == null) { - synchronized (this) { - if (client == null) { - try { - this.client = createMongoClient(); + private void lazyInit(KeycloakSession session) { + if (client == null) { + synchronized (this) { + if (client == null) { + try { + this.client = createMongoClient(); - String dbName = config.get("db", "keycloak"); - this.db = client.getDB(dbName); + String dbName = config.get("db", "keycloak"); + this.db = client.getDB(dbName); - String databaseSchema = config.get("databaseSchema"); - if (databaseSchema != null) { - if (databaseSchema.equals("update")) { - MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); + String databaseSchema = config.get("databaseSchema"); + if (databaseSchema != null) { + if (databaseSchema.equals("update")) { + MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); - if (mongoUpdater == null) { - throw new RuntimeException("Can't update database: Mongo updater provider not found"); - } + if (mongoUpdater == null) { + throw new RuntimeException("Can't update database: Mongo updater provider not found"); + } - mongoUpdater.update(session, db); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } - } + mongoUpdater.update(session, db); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } + } - this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - } - } + this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + } - private Class[] getManagedEntities() throws ClassNotFoundException { - Class[] entityClasses = new Class[entities.length]; - for (int i = 0; i < entities.length; i++) { - entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); - } - return entityClasses; - } + private Class[] getManagedEntities() throws ClassNotFoundException { + Class[] entityClasses = new Class[entities.length]; + for (int i = 0; i < entities.length; i++) { + entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); + } + return entityClasses; + } - @Override - public void close() { - if (client != null) { - client.close(); - } - } + @Override + public void close() { + if (client != null) { + client.close(); + } + } - @Override - public String getId() { - return "default"; - } + @Override + public String getId() { + return "default"; + } - /** - * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client - * from different source. - * - * This method can assume that "config" is already set and can use it. - * - * @return mongoClient instance, which will be shared for whole Keycloak - * - * @throws UnknownHostException - */ - protected MongoClient createMongoClient() throws UnknownHostException { - String host = config.get("host", ServerAddress.defaultHost()); - int port = config.getInt("port", ServerAddress.defaultPort()); - String dbName = config.get("db", "keycloak"); + /** + * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo + * client from different source. + * + * This method can assume that "config" is already set and can use it. + * + * @return mongoClient instance, which will be shared for whole Keycloak + * + * @throws UnknownHostException + */ + protected MongoClient createMongoClient() throws UnknownHostException { + String host = config.get("host", ServerAddress.defaultHost()); + int port = config.getInt("port", ServerAddress.defaultPort()); + String dbName = config.get("db", "keycloak"); - String user = config.get("user"); - String password = config.get("password"); + String user = config.get("user"); + String password = config.get("password"); - MongoClientOptions clientOptions = getClientOptions(); + MongoClientOptions clientOptions = getClientOptions(); - MongoClient client; - if (user != null && password != null) { - MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); - client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); - } else { - client = new MongoClient(new ServerAddress(host, port), clientOptions); - } + MongoClient client; + if (user != null && password != null) { + MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); + client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); + } else { + client = new MongoClient(new ServerAddress(host, port), clientOptions); + } - logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); - return client; - } + mongoDbInfo = new MongoDbInfo(); + mongoDbInfo.driverVersion = client.getVersion(); + mongoDbInfo.address = client.getAddress().toString(); + mongoDbInfo.database = dbName; + mongoDbInfo.user = user; - protected MongoClientOptions getClientOptions() { - MongoClientOptions.Builder builder = MongoClientOptions.builder(); - checkIntOption("connectionsPerHost", builder); - checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); - checkIntOption("maxWaitTime", builder); - checkIntOption("connectTimeout", builder); - checkIntOption("socketTimeout", builder); - checkBooleanOption("socketKeepAlive", builder); - checkBooleanOption("autoConnectRetry", builder); - if (config.getLong("maxAutoConnectRetryTime") != null) { - builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); - } - if(config.getBoolean("ssl", false)) { - builder.socketFactory(SSLSocketFactory.getDefault()); - } + logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); + return client; + } - return builder.build(); - } + protected MongoClientOptions getClientOptions() { + MongoClientOptions.Builder builder = MongoClientOptions.builder(); + checkIntOption("connectionsPerHost", builder); + checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); + checkIntOption("maxWaitTime", builder); + checkIntOption("connectTimeout", builder); + checkIntOption("socketTimeout", builder); + checkBooleanOption("socketKeepAlive", builder); + checkBooleanOption("autoConnectRetry", builder); + if (config.getLong("maxAutoConnectRetryTime") != null) { + builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); + } + if (config.getBoolean("ssl", false)) { + builder.socketFactory(SSLSocketFactory.getDefault()); + } - protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { - Boolean val = config.getBoolean(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); - } - } - } + return builder.build(); + } - protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { - Integer val = config.getInt(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); - } - } - } + protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { + Boolean val = config.getBoolean(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); + } + } + } + + protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { + Integer val = config.getInt(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); + } + } + } + + @Override + public ProviderOperationalInfo getOperationalInfo() { + return mongoDbInfo; + } + + public static class MongoDbInfo implements ProviderOperationalInfo { + + public String address; + public String database; + public String driverVersion; + public String user; + + @Override + public boolean isOk() { + // TODO KEYCLOAK-1578 - implement operational monitoring of Mongo DB connection + return true; + } + + public String getAddress() { + return address; + } + + public String getDatabase() { + return database; + } + + public String getDriverVersion() { + return driverVersion; + } + + public String getUser() { + return user; + } + } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java index e787ce63825..3b3a00e5602 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java @@ -1,9 +1,9 @@ package org.keycloak.connections.mongo; -import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.MonitorableProviderFactory; /** * @author Stian Thorgersen */ -public interface MongoConnectionProviderFactory extends ProviderFactory { +public interface MongoConnectionProviderFactory extends MonitorableProviderFactory { } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 5590f9576e1..f4e4c6dcf26 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -1,21 +1,47 @@
-

Server Info

+

Server Info

- +
+ + + + + + + + + + + + +
Keycloak Version{{serverInfo.version}}
Server Time{{serverInfo.serverTime}} (update)
Server Uptime{{serverInfo.serverUptime}}
+ +
+ Java VM Memory Statistics +
+ + + + + + + + + + + + + +
Total Memory{{serverInfo.memoryInfo.totalFormated}}
Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)
Used Memory{{serverInfo.memoryInfo.usedFormated}}
+
+
+ +
+ System Info +
+ - - - - - - - - - - - - - + @@ -66,23 +92,59 @@ -
Version{{serverInfo.version}}
Server Time{{serverInfo.serverTime}} (update)
Server Uptime{{serverInfo.serverUptime}}
Current Working DirectoryCurrent Working Directory {{serverInfo.systemInfo.userDir}}
OS Architecture {{serverInfo.systemInfo.osArchitecture}}
- -

Java VM Memory Statistics

- - - - - - - - - - - - - -
Total Memory{{serverInfo.memoryInfo.totalFormated}}
Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)
Used Memory{{serverInfo.memoryInfo.usedFormated}}
+ +
+
+ + +
+ Database Info +
+ + + + + + + + + + + + + + + + + +
Database URL{{serverInfo.jpaInfo.jdbcUrl}}
Database User{{serverInfo.jpaInfo.databaseUser}}
Database Type{{serverInfo.jpaInfo.databaseProduct}}
Database Driver{{serverInfo.jpaInfo.databaseDriver}}
+
+
+ +
+ Mongo DB Info +
+ + + + + + + + + + + + + + + + + +
Address{{serverInfo.mongoDbInfo.address}}
Database{{serverInfo.mongoDbInfo.database}}
User{{serverInfo.mongoDbInfo.user}}
Driver Version{{serverInfo.mongoDbInfo.driverVersion}}
+
+
+
Providers @@ -93,7 +155,7 @@ - + @@ -117,7 +179,7 @@
SPISPI Providers
- + diff --git a/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java new file mode 100644 index 00000000000..7b9c95c9218 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java @@ -0,0 +1,18 @@ +package org.keycloak.provider; + +/** + * Provider factory for provider which is monitorable. It means some info about it can be shown on "Server Info" page or accessed over Operational monitoring endpoint. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public interface MonitorableProviderFactory extends ProviderFactory { + + /** + * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc). + * Is used to be shown on "Server Info" page or in Operational monitoring endpoint. + * + * @return extendion of {@link ProviderOperationalInfo} + */ + public ProviderOperationalInfo getOperationalInfo(); + +} diff --git a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java new file mode 100644 index 00000000000..ca3e8a6849a --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java @@ -0,0 +1,22 @@ +package org.keycloak.provider; + +import java.io.Serializable; + +/** + * Operational info about given Provider. + * Contains info about Provider that can be shown on "Server Info" page or accessed over Operational monitoring endpoint. + * + * @author Vlastimil Elias (velias at redhat dot com) + * @see MonitorableProviderFactory + */ +public interface ProviderOperationalInfo extends Serializable { + + /** + * Return true if provider is OK from operation point of view. It means it is able to perform necessary work. + * It can return false for example if remote DB of JPA provider is not available, or LDAP server of LDAP based user federation provider is not available. + * + * @return true if provider is OK to perform his operation. + */ + boolean isOk(); + +} diff --git a/services/pom.xml b/services/pom.xml index da71e9ae6f6..16fef06d5cb 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -39,6 +39,21 @@ keycloak-connections-http-client provided + + org.keycloak + keycloak-connections-jpa + provided + + + org.hibernate + hibernate-entitymanager + provided + + + org.keycloak + keycloak-connections-mongo + provided + org.keycloak keycloak-forms-common-freemarker diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index c600b92e4b4..91610db0c43 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,5 +1,6 @@ package org.keycloak.services.resources.admin; +import java.io.Serializable; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -13,9 +14,13 @@ import java.util.Set; import javax.ws.rs.GET; import javax.ws.rs.core.Context; +import org.jboss.logging.Logger; import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.connections.mongo.DefaultMongoConnectionFactoryProvider.MongoDbInfo; +import org.keycloak.connections.mongo.MongoConnectionProvider; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; @@ -29,8 +34,10 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.provider.MonitorableProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.ProviderOperationalInfo; import org.keycloak.provider.Spi; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -41,6 +48,8 @@ import org.keycloak.social.SocialIdentityProvider; * @author Stian Thorgersen */ public class ServerInfoAdminResource { + + private static final Logger logger = Logger.getLogger(ServerInfoAdminResource.class); private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class); @@ -58,6 +67,8 @@ public class ServerInfoAdminResource { info.version = Version.VERSION; info.serverTime = new Date().toString(); info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); + info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); + info.systemInfo = (new SystemInfo()).init(); setSocialProviders(info); setIdentityProviders(info); setThemes(info); @@ -68,6 +79,21 @@ public class ServerInfoAdminResource { setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); info.setEnums(ENUMS); + + ProviderFactory jpf = session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); + if(jpf!=null && jpf instanceof MonitorableProviderFactory){ + info.jpaInfo = ((MonitorableProviderFactory)jpf).getOperationalInfo(); + } else { + logger.debug("JPA provider not found or is not monitorable"); + } + + ProviderFactory mpf = session.getKeycloakSessionFactory().getProviderFactory(MongoConnectionProvider.class); + if(mpf!=null && mpf instanceof MonitorableProviderFactory){ + info.mongoDbInfo = ((MonitorableProviderFactory)mpf).getOperationalInfo(); + } else { + logger.debug("Mongo provider not found or is not monitorable"); + } + return info; } @@ -191,10 +217,28 @@ public class ServerInfoAdminResource { } } - public static class MemoryInfo{ + public static class MemoryInfo implements Serializable { + + protected long total; + + protected long used; + + public MemoryInfo(){ + } + + /** + * Fill object fwith info. + * @param runtime used to get memory info from. + * @return itself for chaining + */ + public MemoryInfo init(Runtime runtime){ + total = runtime.maxMemory(); + used = runtime.totalMemory() - runtime.freeMemory(); + return this; + } public long getTotal(){ - return Runtime.getRuntime().maxMemory(); + return total; } public String getTotalFormated(){ @@ -210,7 +254,7 @@ public class ServerInfoAdminResource { } public long getUsed(){ - return Runtime.getRuntime().totalMemory(); + return used; } public String getUsedFormated(){ @@ -218,7 +262,7 @@ public class ServerInfoAdminResource { } public long getFreePercentage(){ - return getFree()*100/getTotal(); + return getFree() * 100 / getTotal(); } private String formatMemory(long bytes){ @@ -233,71 +277,109 @@ public class ServerInfoAdminResource { } - public static class SystemInfo { + public static class SystemInfo implements Serializable { + + protected String javaVersion; + protected String javaVendor; + protected String javaVm; + protected String javaVmVersion; + protected String javaRuntime; + protected String javaHome; + protected String osName; + protected String osArchitecture; + protected String osVersion; + protected String fileEncoding; + protected String userName; + protected String userDir; + protected String userTimezone; + protected String userLocale; + + public SystemInfo() { + } + + /** + * Fill object with info about current system loaded from {@link System} properties. + * @return object itself for chaining + */ + protected SystemInfo init(){ + javaVersion = System.getProperty("java.version"); + javaVendor = System.getProperty("java.vendor"); + javaVm = System.getProperty("java.vm.name"); + javaVmVersion = System.getProperty("java.vm.version"); + javaRuntime = System.getProperty("java.runtime.name"); + javaHome = System.getProperty("java.home"); + osName = System.getProperty("os.name"); + osArchitecture = System.getProperty("os.arch"); + osVersion = System.getProperty("os.version"); + fileEncoding = System.getProperty("file.encoding"); + userName = System.getProperty("user.name"); + userDir = System.getProperty("user.dir"); + userTimezone = System.getProperty("user.timezone"); + userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return this; + } + public String getJavaVersion(){ - return System.getProperty("java.version"); + return javaVersion; } public String getJavaVendor(){ - return System.getProperty("java.vendor"); + return javaVendor; } public String getJavaVm(){ - return System.getProperty("java.vm.name"); + return javaVm; } public String getJavaVmVersion(){ - return System.getProperty("java.vm.version"); + return javaVmVersion; } public String getJavaRuntime(){ - return System.getProperty("java.runtime.name"); + return javaRuntime; } public String getJavaHome(){ - return System.getProperty("java.home"); + return javaHome; } public String getOsName(){ - return System.getProperty("os.name"); + return osName; } public String getOsArchitecture(){ - return System.getProperty("os.arch"); + return osArchitecture; } public String getOsVersion(){ - return System.getProperty("os.version"); + return osVersion; } public String getFileEncoding(){ - return System.getProperty("file.encoding"); + return fileEncoding; } public String getUserName(){ - return System.getProperty("user.name"); + return userName; } public String getUserDir(){ - return System.getProperty("user.dir"); + return userDir; } public String getUserTimezone(){ - return System.getProperty("user.timezone"); + return userTimezone; } public String getUserLocale(){ - return (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return userLocale; } - } - public static class ServerInfoRepresentation { + public static class ServerInfoRepresentation implements Serializable { private String version; - private String serverTime; - private long serverStartupTime; private Map> themes; @@ -314,26 +396,44 @@ public class ServerInfoAdminResource { private Map> builtinProtocolMappers; private Map> enums; + + private MemoryInfo memoryInfo; + private SystemInfo systemInfo; + + private ProviderOperationalInfo jpaInfo; + private ProviderOperationalInfo mongoDbInfo; public ServerInfoRepresentation() { } public SystemInfo getSystemInfo(){ - return new SystemInfo(); + return systemInfo; } public MemoryInfo getMemoryInfo(){ - return new MemoryInfo(); + return memoryInfo; + } + + public ProviderOperationalInfo getJpaInfo() { + return jpaInfo; + } + + public ProviderOperationalInfo getMongoDbInfo() { + return mongoDbInfo; } public String getServerTime() { return serverTime; } + public long getServerStartupTime() { + return serverStartupTime; + } + /** * @return server startup time formatted */ - public String getServerStartupTime() { + public String getServerStartupTimeFormatted() { return (new Date(serverStartupTime)).toString(); } @@ -420,7 +520,7 @@ public class ServerInfoAdminResource { } } - public static class SpiInfoRepresentation { + public static class SpiInfoRepresentation implements Serializable { private String name; private boolean internal; private Set implementations; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 3bde1cb7406..85a81bbbebd 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -21,10 +21,27 @@ */ package org.keycloak.testsuite.admin; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.Config; +import org.keycloak.Version; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -40,22 +57,8 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminRoot; -import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.KeycloakServer; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; /** * Tests Undertow Adapter @@ -295,4 +298,34 @@ public class AdminAPITest { testCreateRealm("/admin-test/testrealm.json"); } + @Test + public void testServerInfo() { + + String token = createToken(); + final String authHeader = "Bearer " + token; + ClientRequestFilter authFilter = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); + } + }; + Client client = ClientBuilder.newBuilder().register(authFilter).build(); + UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth"); + WebTarget target = client.target(AdminRoot.adminBaseUrl(authBase).path("serverinfo")); + + Map response = target.request().accept("application/json").get(Map.class); + + Assert.assertNotNull(response); + Assert.assertEquals(Version.VERSION, response.get("version")); + Assert.assertNotNull(response.get("serverTime")); + Assert.assertNotNull(response.get("serverStartupTime")); + + Assert.assertNotNull(response.get("memoryInfo")); + Assert.assertNotNull(response.get("jpaInfo")); + Assert.assertNull(response.get("mongoDbInfo")); + + System.out.println(response); + + } + } From 85816be96716b46fd6d622601764af8a9b7f7339 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Wed, 22 Jul 2015 13:35:56 +0200 Subject: [PATCH 03/13] KEYCLOAK-1542 - code cleanup ahead of PR --- .../jpa/DefaultJpaConnectionProviderFactory.java | 6 ------ .../mongo/DefaultMongoConnectionFactoryProvider.java | 6 ------ .../org/keycloak/provider/ProviderOperationalInfo.java | 8 -------- .../services/resources/admin/ServerInfoAdminResource.java | 1 - 4 files changed, 21 deletions(-) diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 36f3c09cce7..1795cd13c5f 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -231,12 +231,6 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide public String getDatabaseProduct() { return databaseProduct; } - - @Override - public boolean isOk() { - // TODO KEYCLOAK-1578 - implement operational monitoring of JPA DB connection - return true; - } } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index c8d1cbaed4a..248b880be6c 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -212,12 +212,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro public String driverVersion; public String user; - @Override - public boolean isOk() { - // TODO KEYCLOAK-1578 - implement operational monitoring of Mongo DB connection - return true; - } - public String getAddress() { return address; } diff --git a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java index ca3e8a6849a..e37544d2768 100644 --- a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java +++ b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java @@ -11,12 +11,4 @@ import java.io.Serializable; */ public interface ProviderOperationalInfo extends Serializable { - /** - * Return true if provider is OK from operation point of view. It means it is able to perform necessary work. - * It can return false for example if remote DB of JPA provider is not available, or LDAP server of LDAP based user federation provider is not available. - * - * @return true if provider is OK to perform his operation. - */ - boolean isOk(); - } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index 91610db0c43..dbb0ff0c8f1 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -19,7 +19,6 @@ import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.connections.mongo.DefaultMongoConnectionFactoryProvider.MongoDbInfo; import org.keycloak.connections.mongo.MongoConnectionProvider; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; From 1c142cb17074bf91f73e3f7ded7fa307da1e8afb Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Wed, 22 Jul 2015 14:28:08 +0200 Subject: [PATCH 04/13] KEYCLOAK-1542 - code formatting cleanup --- .../DefaultJpaConnectionProviderFactory.java | 385 +++++++++--------- ...DefaultMongoConnectionFactoryProvider.java | 377 +++++++++-------- .../admin/ServerInfoAdminResource.java | 28 +- .../testsuite/admin/AdminAPITest.java | 33 +- 4 files changed, 420 insertions(+), 403 deletions(-) diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 1795cd13c5f..aac1cdebd47 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -1,19 +1,5 @@ package org.keycloak.connections.jpa; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; - -import javax.naming.InitialContext; -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.Persistence; -import javax.sql.DataSource; - import org.hibernate.ejb.AvailableSettings; import org.jboss.logging.Logger; import org.keycloak.Config; @@ -22,215 +8,232 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderOperationalInfo; +import javax.naming.InitialContext; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + /** * @author Stian Thorgersen */ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory { - private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); + private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); - private volatile EntityManagerFactory emf; + private volatile EntityManagerFactory emf; - private Config.Scope config; + private Config.Scope config; + + private DatabaseInfo databaseInfo; - private DatabaseInfo databaseInfo; + @Override + public JpaConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public JpaConnectionProvider create(KeycloakSession session) { - lazyInit(session); + EntityManager em = emf.createEntityManager(); + em = PersistenceExceptionConverter.create(em); + session.getTransaction().enlist(new JpaKeycloakTransaction(em)); + return new DefaultJpaConnectionProvider(em); + } - EntityManager em = emf.createEntityManager(); - em = PersistenceExceptionConverter.create(em); - session.getTransaction().enlist(new JpaKeycloakTransaction(em)); - return new DefaultJpaConnectionProvider(em); - } + @Override + public void close() { + if (emf != null) { + emf.close(); + } + } - @Override - public void close() { - if (emf != null) { - emf.close(); - } - } + @Override + public String getId() { + return "default"; + } - @Override - public String getId() { - return "default"; - } + @Override + public void init(Config.Scope config) { + this.config = config; + } - @Override - public void init(Config.Scope config) { - this.config = config; - } + @Override + public void postInit(KeycloakSessionFactory factory) { - @Override - public void postInit(KeycloakSessionFactory factory) { + } - } + private void lazyInit(KeycloakSession session) { + if (emf == null) { + synchronized (this) { + if (emf == null) { + logger.debug("Initializing JPA connections"); - private void lazyInit(KeycloakSession session) { - if (emf == null) { - synchronized (this) { - if (emf == null) { - logger.debug("Initializing JPA connections"); + Connection connection = null; - Connection connection = null; + String databaseSchema = config.get("databaseSchema"); - String databaseSchema = config.get("databaseSchema"); + Map properties = new HashMap(); - Map properties = new HashMap(); + String unitName = "keycloak-default"; - String unitName = "keycloak-default"; + String dataSource = config.get("dataSource"); + if (dataSource != null) { + if (config.getBoolean("jta", false)) { + properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); + } else { + properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); + } + } else { + properties.put(AvailableSettings.JDBC_URL, config.get("url")); + properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); - String dataSource = config.get("dataSource"); - if (dataSource != null) { - if (config.getBoolean("jta", false)) { - properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); - } else { - properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); - } - } else { - properties.put(AvailableSettings.JDBC_URL, config.get("url")); - properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); + String user = config.get("user"); + if (user != null) { + properties.put(AvailableSettings.JDBC_USER, user); + } + String password = config.get("password"); + if (password != null) { + properties.put(AvailableSettings.JDBC_PASSWORD, password); + } + } - String user = config.get("user"); - if (user != null) { - properties.put(AvailableSettings.JDBC_USER, user); - } - String password = config.get("password"); - if (password != null) { - properties.put(AvailableSettings.JDBC_PASSWORD, password); - } - } + String driverDialect = config.get("driverDialect"); + if (driverDialect != null && driverDialect.length() > 0) { + properties.put("hibernate.dialect", driverDialect); + } - String driverDialect = config.get("driverDialect"); - if (driverDialect != null && driverDialect.length() > 0) { - properties.put("hibernate.dialect", driverDialect); - } + String schema = config.get("schema"); + if (schema != null) { + properties.put("hibernate.default_schema", schema); + } - String schema = config.get("schema"); - if (schema != null) { - properties.put("hibernate.default_schema", schema); - } + if (databaseSchema != null) { + if (databaseSchema.equals("development-update")) { + properties.put("hibernate.hbm2ddl.auto", "update"); + databaseSchema = null; + } else if (databaseSchema.equals("development-validate")) { + properties.put("hibernate.hbm2ddl.auto", "validate"); + databaseSchema = null; + } + } - if (databaseSchema != null) { - if (databaseSchema.equals("development-update")) { - properties.put("hibernate.hbm2ddl.auto", "update"); - databaseSchema = null; - } else if (databaseSchema.equals("development-validate")) { - properties.put("hibernate.hbm2ddl.auto", "validate"); - databaseSchema = null; - } - } + properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); + properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); - properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); - properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); + connection = getConnection(); + try{ + prepareDatabaseInfo(connection); + + if (databaseSchema != null) { + logger.trace("Updating database"); + + JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); + if (updater == null) { + throw new RuntimeException("Can't update database: JPA updater provider not found"); + } + + if (databaseSchema.equals("update")) { + String currentVersion = null; + try { + ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); + if (resultSet.next()) { + currentVersion = resultSet.getString(1); + } + } catch (SQLException e) { + } + + if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { + updater.update(session, connection, schema); + } else { + logger.debug("Database is up to date"); + } + } else if (databaseSchema.equals("validate")) { + updater.validate(connection, schema); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } + + logger.trace("Database update completed"); + } + + logger.trace("Creating EntityManagerFactory"); + emf = Persistence.createEntityManagerFactory(unitName, properties); + logger.trace("EntityManagerFactory created"); - connection = getConnection(); - prepareDatabaseInfo(connection); + } finally { + // Close after creating EntityManagerFactory to prevent in-mem databases from closing + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + logger.warn(e); + } + } + } + } + } + } + } + + protected void prepareDatabaseInfo(Connection connection) { + try { + databaseInfo = new DatabaseInfo(); + DatabaseMetaData md = connection.getMetaData(); + databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); + databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); + databaseInfo.databaseUser = md.getUserName(); + databaseInfo.jdbcUrl = md.getURL(); + } catch (SQLException e) { + logger.warn("Unable to get database info due " + e.getMessage()); + } + } - if (databaseSchema != null) { - logger.trace("Updating database"); + private Connection getConnection() { + try { + String dataSourceLookup = config.get("dataSource"); + if (dataSourceLookup != null) { + DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); + return dataSource.getConnection(); + } else { + Class.forName(config.get("driver")); + return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); + } + } catch (Exception e) { + throw new RuntimeException("Failed to connect to database", e); + } + } + + @Override + public DatabaseInfo getOperationalInfo() { + return databaseInfo; + } - JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); - if (updater == null) { - throw new RuntimeException("Can't update database: JPA updater provider not found"); - } + public static class DatabaseInfo implements ProviderOperationalInfo { + protected String jdbcUrl; + protected String databaseUser; + protected String databaseProduct; + protected String databaseDriver; - if (databaseSchema.equals("update")) { - String currentVersion = null; - try { - ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); - if (resultSet.next()) { - currentVersion = resultSet.getString(1); - } - } catch (SQLException e) { - } + public String getJdbcUrl() { + return jdbcUrl; + } - if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { - updater.update(session, connection, schema); - } else { - logger.debug("Database is up to date"); - } - } else if (databaseSchema.equals("validate")) { - updater.validate(connection, schema); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } + public String getDatabaseDriver() { + return databaseDriver; + } - logger.trace("Database update completed"); - } + public String getDatabaseUser() { + return databaseUser; + } - logger.trace("Creating EntityManagerFactory"); - emf = Persistence.createEntityManagerFactory(unitName, properties); - logger.trace("EntityManagerFactory created"); - - // Close after creating EntityManagerFactory to prevent in-mem databases from closing - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - logger.warn(e); - } - } - } - } - } - } - - protected void prepareDatabaseInfo(Connection connection) { - try { - databaseInfo = new DatabaseInfo(); - DatabaseMetaData md = connection.getMetaData(); - databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); - databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); - databaseInfo.databaseUser = md.getUserName(); - databaseInfo.jdbcUrl = md.getURL(); - } catch (SQLException e) { - logger.warn("Unable to get database info due " + e.getMessage()); - } - } - - private Connection getConnection() { - try { - String dataSourceLookup = config.get("dataSource"); - if (dataSourceLookup != null) { - DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); - return dataSource.getConnection(); - } else { - Class.forName(config.get("driver")); - return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); - } - } catch (Exception e) { - throw new RuntimeException("Failed to connect to database", e); - } - } - - @Override - public DatabaseInfo getOperationalInfo() { - return databaseInfo; - } - - public static class DatabaseInfo implements ProviderOperationalInfo { - protected String jdbcUrl; - protected String databaseUser; - protected String databaseProduct; - protected String databaseDriver; - - public String getJdbcUrl() { - return jdbcUrl; - } - - public String getDatabaseDriver() { - return databaseDriver; - } - - public String getDatabaseUser() { - return databaseUser; - } - - public String getDatabaseProduct() { - return databaseProduct; - } - } + public String getDatabaseProduct() { + return databaseProduct; + } + } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 248b880be6c..2df4b853630 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -1,10 +1,10 @@ package org.keycloak.connections.mongo; -import java.lang.reflect.Method; -import java.net.UnknownHostException; -import java.util.Collections; - -import javax.net.ssl.SSLSocketFactory; +import com.mongodb.DB; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; import org.jboss.logging.Logger; import org.keycloak.Config; @@ -16,217 +16,234 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderOperationalInfo; -import com.mongodb.DB; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; +import javax.net.ssl.SSLSocketFactory; + +import java.lang.reflect.Method; +import java.net.UnknownHostException; +import java.util.Collections; /** * @author Stian Thorgersen */ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { - // TODO Make configurable - private String[] entities = new String[] { "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", - "org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.CredentialEntity", - "org.keycloak.models.entities.FederatedIdentityEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", - "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", "org.keycloak.models.entities.UserFederationProviderEntity", - "org.keycloak.models.entities.UserFederationMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity", "org.keycloak.models.entities.IdentityProviderMapperEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.entities.AuthenticationExecutionEntity", - "org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.RequiredActionProviderEntity", }; + // TODO Make configurable + private String[] entities = new String[]{ + "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", + "org.keycloak.models.entities.IdentityProviderEntity", + "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", + "org.keycloak.models.entities.RequiredCredentialEntity", + "org.keycloak.models.entities.CredentialEntity", + "org.keycloak.models.entities.FederatedIdentityEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", + "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", + "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", + "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", + "org.keycloak.models.entities.UserFederationProviderEntity", + "org.keycloak.models.entities.UserFederationMapperEntity", + "org.keycloak.models.entities.ProtocolMapperEntity", + "org.keycloak.models.entities.IdentityProviderMapperEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", + "org.keycloak.models.entities.AuthenticationExecutionEntity", + "org.keycloak.models.entities.AuthenticationFlowEntity", + "org.keycloak.models.entities.AuthenticatorConfigEntity", + "org.keycloak.models.entities.RequiredActionProviderEntity", + }; - private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); + private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); - private volatile MongoClient client; + private volatile MongoClient client; - private MongoStore mongoStore; - private DB db; - protected Config.Scope config; + private MongoStore mongoStore; + private DB db; + protected Config.Scope config; + + private MongoDbInfo mongoDbInfo; - private MongoDbInfo mongoDbInfo; + @Override + public MongoConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public MongoConnectionProvider create(KeycloakSession session) { - lazyInit(session); + TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); + session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); + return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); + } - TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); - session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); - return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); - } + @Override + public void init(Config.Scope config) { + this.config = config; + } - @Override - public void init(Config.Scope config) { - this.config = config; - } + @Override + public void postInit(KeycloakSessionFactory factory) { - @Override - public void postInit(KeycloakSessionFactory factory) { + } - } - private void lazyInit(KeycloakSession session) { - if (client == null) { - synchronized (this) { - if (client == null) { - try { - this.client = createMongoClient(); + private void lazyInit(KeycloakSession session) { + if (client == null) { + synchronized (this) { + if (client == null) { + try { + this.client = createMongoClient(); - String dbName = config.get("db", "keycloak"); - this.db = client.getDB(dbName); + String dbName = config.get("db", "keycloak"); + this.db = client.getDB(dbName); - String databaseSchema = config.get("databaseSchema"); - if (databaseSchema != null) { - if (databaseSchema.equals("update")) { - MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); + String databaseSchema = config.get("databaseSchema"); + if (databaseSchema != null) { + if (databaseSchema.equals("update")) { + MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); - if (mongoUpdater == null) { - throw new RuntimeException("Can't update database: Mongo updater provider not found"); - } + if (mongoUpdater == null) { + throw new RuntimeException("Can't update database: Mongo updater provider not found"); + } - mongoUpdater.update(session, db); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } - } + mongoUpdater.update(session, db); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } + } - this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - } - } + this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + } - private Class[] getManagedEntities() throws ClassNotFoundException { - Class[] entityClasses = new Class[entities.length]; - for (int i = 0; i < entities.length; i++) { - entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); - } - return entityClasses; - } + private Class[] getManagedEntities() throws ClassNotFoundException { + Class[] entityClasses = new Class[entities.length]; + for (int i = 0; i < entities.length; i++) { + entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); + } + return entityClasses; + } - @Override - public void close() { - if (client != null) { - client.close(); - } - } + @Override + public void close() { + if (client != null) { + client.close(); + } + } - @Override - public String getId() { - return "default"; - } + @Override + public String getId() { + return "default"; + } - /** - * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo - * client from different source. - * - * This method can assume that "config" is already set and can use it. - * - * @return mongoClient instance, which will be shared for whole Keycloak - * - * @throws UnknownHostException - */ - protected MongoClient createMongoClient() throws UnknownHostException { - String host = config.get("host", ServerAddress.defaultHost()); - int port = config.getInt("port", ServerAddress.defaultPort()); - String dbName = config.get("db", "keycloak"); + /** + * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client + * from different source. + * + * This method can assume that "config" is already set and can use it. + * + * @return mongoClient instance, which will be shared for whole Keycloak + * + * @throws UnknownHostException + */ + protected MongoClient createMongoClient() throws UnknownHostException { + String host = config.get("host", ServerAddress.defaultHost()); + int port = config.getInt("port", ServerAddress.defaultPort()); + String dbName = config.get("db", "keycloak"); - String user = config.get("user"); - String password = config.get("password"); + String user = config.get("user"); + String password = config.get("password"); - MongoClientOptions clientOptions = getClientOptions(); + MongoClientOptions clientOptions = getClientOptions(); - MongoClient client; - if (user != null && password != null) { - MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); - client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); - } else { - client = new MongoClient(new ServerAddress(host, port), clientOptions); - } + MongoClient client; + if (user != null && password != null) { + MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); + client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); + } else { + client = new MongoClient(new ServerAddress(host, port), clientOptions); + } + + mongoDbInfo = new MongoDbInfo(); + mongoDbInfo.driverVersion = client.getVersion(); + mongoDbInfo.address = client.getAddress().toString(); + mongoDbInfo.database = dbName; + mongoDbInfo.user = user; + + logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); + return client; + } - mongoDbInfo = new MongoDbInfo(); - mongoDbInfo.driverVersion = client.getVersion(); - mongoDbInfo.address = client.getAddress().toString(); - mongoDbInfo.database = dbName; - mongoDbInfo.user = user; + protected MongoClientOptions getClientOptions() { + MongoClientOptions.Builder builder = MongoClientOptions.builder(); + checkIntOption("connectionsPerHost", builder); + checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); + checkIntOption("maxWaitTime", builder); + checkIntOption("connectTimeout", builder); + checkIntOption("socketTimeout", builder); + checkBooleanOption("socketKeepAlive", builder); + checkBooleanOption("autoConnectRetry", builder); + if (config.getLong("maxAutoConnectRetryTime") != null) { + builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); + } + if(config.getBoolean("ssl", false)) { + builder.socketFactory(SSLSocketFactory.getDefault()); + } - logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); - return client; - } + return builder.build(); + } - protected MongoClientOptions getClientOptions() { - MongoClientOptions.Builder builder = MongoClientOptions.builder(); - checkIntOption("connectionsPerHost", builder); - checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); - checkIntOption("maxWaitTime", builder); - checkIntOption("connectTimeout", builder); - checkIntOption("socketTimeout", builder); - checkBooleanOption("socketKeepAlive", builder); - checkBooleanOption("autoConnectRetry", builder); - if (config.getLong("maxAutoConnectRetryTime") != null) { - builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); - } - if (config.getBoolean("ssl", false)) { - builder.socketFactory(SSLSocketFactory.getDefault()); - } + protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { + Boolean val = config.getBoolean(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); + } + } + } - return builder.build(); - } + protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { + Integer val = config.getInt(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); + } + } + } + + @Override + public ProviderOperationalInfo getOperationalInfo() { + return mongoDbInfo; + } - protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { - Boolean val = config.getBoolean(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); - } - } - } + public static class MongoDbInfo implements ProviderOperationalInfo { - protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { - Integer val = config.getInt(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); - } - } - } + public String address; + public String database; + public String driverVersion; + public String user; - @Override - public ProviderOperationalInfo getOperationalInfo() { - return mongoDbInfo; - } + public String getAddress() { + return address; + } - public static class MongoDbInfo implements ProviderOperationalInfo { + public String getDatabase() { + return database; + } - public String address; - public String database; - public String driverVersion; - public String user; - - public String getAddress() { - return address; - } - - public String getDatabase() { - return database; - } - - public String getDriverVersion() { - return driverVersion; - } - - public String getUser() { - return user; - } - } + public String getDriverVersion() { + return driverVersion; + } + public String getUser() { + return user; + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index dbb0ff0c8f1..3b69304b065 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,19 +1,5 @@ package org.keycloak.services.resources.admin; -import java.io.Serializable; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; - -import javax.ws.rs.GET; -import javax.ws.rs.core.Context; - import org.jboss.logging.Logger; import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; @@ -43,6 +29,19 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.social.SocialIdentityProvider; +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; +import java.io.Serializable; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + /** * @author Stian Thorgersen */ @@ -219,7 +218,6 @@ public class ServerInfoAdminResource { public static class MemoryInfo implements Serializable { protected long total; - protected long used; public MemoryInfo(){ diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 85a81bbbebd..2cca19a0c98 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -21,22 +21,6 @@ */ package org.keycloak.testsuite.admin; -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; - import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -57,8 +41,23 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminRoot; -import org.keycloak.testsuite.KeycloakServer; import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.KeycloakServer; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Tests Undertow Adapter From c99785f2666cf4702b74c0d490391e3843edbab9 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 22 Jul 2015 19:48:28 +0200 Subject: [PATCH 05/13] KEYCLOAK-401 Service account refactoring and fixes --- .../META-INF/jpa-changelog-1.4.0.xml | 1 + .../constants/ServiceAccountConstants.java | 1 - .../idm/UserRepresentation.java | 9 ++ .../example/ProductServiceAccountServlet.java | 10 +- examples/demo-template/testrealm.json | 7 ++ .../exportimport/util/ExportUtils.java | 11 +- .../util/MultipleStepsExportProvider.java | 2 +- .../theme/base/admin/resources/js/app.js | 15 ++- .../admin/resources/js/controllers/clients.js | 20 ---- .../admin/resources/js/controllers/users.js | 43 +++---- .../theme/base/admin/resources/js/loaders.js | 9 ++ .../theme/base/admin/resources/js/services.js | 7 ++ .../resources/partials/client-detail.html | 7 ++ .../client-service-account-roles.html | 113 ++++++++++++++++++ .../partials/client-service-accounts.html | 28 ----- .../resources/partials/role-mappings.html | 6 +- .../resources/templates/kc-tabs-client.html | 6 +- .../migration/migrators/MigrateTo1_4_0.java | 2 +- .../models/UserFederationManager.java | 17 ++- .../java/org/keycloak/models/UserModel.java | 3 + .../org/keycloak/models/UserProvider.java | 7 +- .../keycloak/models/entities/UserEntity.java | 9 ++ .../models/utils/KeycloakModelUtils.java | 3 + .../models/utils/RepresentationToModel.java | 8 ++ .../models/utils/UserModelDelegate.java | 10 ++ .../models/file/FileUserProvider.java | 36 +++++- .../models/file/adapter/UserAdapter.java | 10 ++ .../cache/DefaultCacheUserProvider.java | 13 +- .../models/cache/NoCacheUserProvider.java | 13 +- .../keycloak/models/cache/UserAdapter.java | 12 ++ .../models/cache/entities/CachedUser.java | 6 + .../keycloak/models/jpa/JpaUserProvider.java | 28 ++++- .../org/keycloak/models/jpa/UserAdapter.java | 10 ++ .../models/jpa/entities/UserEntity.java | 16 ++- .../keycloak/adapters/MongoUserProvider.java | 28 ++++- .../mongo/keycloak/adapters/UserAdapter.java | 13 +- .../protocol/oidc/ServiceAccountManager.java | 15 +-- .../services/managers/ClientManager.java | 17 +-- .../resources/admin/ClientResource.java | 26 ++++ .../admin/IdentityProviderResource.java | 2 +- .../resources/admin/UsersResource.java | 2 +- .../broker/AbstractIdentityProviderTest.java | 2 +- .../federation/AbstractKerberosTest.java | 4 +- .../federation/SyncProvidersTest.java | 2 +- .../keycloak/testsuite/model/AdapterTest.java | 2 +- .../keycloak/testsuite/model/ImportTest.java | 8 ++ .../testsuite/model/UserModelTest.java | 56 +++++++++ .../src/test/resources/model/testrealm.json | 6 + 48 files changed, 534 insertions(+), 147 deletions(-) create mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html delete mode 100644 forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml index 103c7cea697..5fc0f23b5ae 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml @@ -142,6 +142,7 @@ + diff --git a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java index 928f62d915b..561a5d078b1 100644 --- a/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java +++ b/core/src/main/java/org/keycloak/constants/ServiceAccountConstants.java @@ -8,7 +8,6 @@ public interface ServiceAccountConstants { String CLIENT_AUTH = "client_auth"; String SERVICE_ACCOUNT_USER_PREFIX = "service-account-"; - String SERVICE_ACCOUNT_CLIENT_ATTRIBUTE = "serviceAccountClient"; String CLIENT_ID_PROTOCOL_MAPPER = "Client ID"; String CLIENT_HOST_PROTOCOL_MAPPER = "Client Host"; diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 1d2bee3c639..ea20afc7083 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -26,6 +26,7 @@ public class UserRepresentation { protected String lastName; protected String email; protected String federationLink; + protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID) // Currently there is Map> but for backwards compatibility, we also need to support Map protected Map attributes; @@ -218,4 +219,12 @@ public class UserRepresentation { public void setFederationLink(String federationLink) { this.federationLink = federationLink; } + + public String getServiceAccountClientId() { + return serviceAccountClientId; + } + + public void setServiceAccountClientId(String serviceAccountClientId) { + this.serviceAccountClientId = serviceAccountClientId; + } } diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java index f9dc9f165e6..d03654d542b 100644 --- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -140,15 +140,7 @@ public class ProductServiceAccountServlet extends HttpServlet { int status = response.getStatusLine().getStatusCode(); if (status != 200) { String json = getContent(entity); - String error = "Failed retrieve products."; - - if (status == 401) { - error = error + " You need to login first with the service account."; - } else if (status == 403) { - error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" + - ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again."; - } - error = error + " Status: " + status + ", Response: " + json; + String error = "Failed retrieve products. Status: " + status + ", Response: " + json; req.setAttribute(ERROR, error); } else if (entity == null) { req.setAttribute(ERROR, "No entity"); diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index a26a0582097..d669e6bccd5 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -71,6 +71,13 @@ "clientRoles": { "realm-management": [ "realm-admin" ] } + }, + { + "username" : "service-account-product-sa-client", + "enabled": true, + "email" : "service-account-product-sa-client@placeholder.org", + "serviceAccountClientId": "product-sa-client", + "realmRoles": [ "user" ] } ], "roles" : { diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index a3396e3f1a5..601ddaf9fa9 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -123,7 +123,7 @@ public class ExportUtils { // Finally users if needed if (includeUsers) { - List allUsers = session.users().getUsers(realm); + List allUsers = session.users().getUsers(realm, true); List users = new ArrayList(); for (UserModel user : allUsers) { UserRepresentation userRep = exportUser(session, realm, user); @@ -286,6 +286,15 @@ public class ExportUtils { userRep.setClientConsents(consentReps); } + // Service account + if (user.getServiceAccountClientLink() != null) { + String clientInternalId = user.getServiceAccountClientLink(); + ClientModel client = realm.getClientById(clientInternalId); + if (client != null) { + userRep.setServiceAccountClientId(client.getClientId()); + } + } + return userRep; } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java index c72708b362b..0ecc10a1ce0 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java @@ -92,7 +92,7 @@ public abstract class MultipleStepsExportProvider implements ExportProvider { @Override protected void runExportImportTask(KeycloakSession session) throws IOException { RealmModel realm = session.realms().getRealmByName(realmName); - usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart); + usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart, true); writeUsers(realmName + "-users-" + (usersHolder.currentPageStart / countPerPage) + ".json", session, realm, usersHolder.users); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 5ddf659deb2..8fb7c36fed2 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -368,6 +368,9 @@ module.config([ '$routeProvider', function($routeProvider) { }, clients : function(ClientListLoader) { return ClientListLoader(); + }, + client : function() { + return {}; } }, controller : 'UserRoleMappingCtrl' @@ -762,17 +765,23 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientInstallationCtrl' }) - .when('/realms/:realm/clients/:client/service-accounts', { - templateUrl : resourceUrl + '/partials/client-service-accounts.html', + .when('/realms/:realm/clients/:client/service-account-roles', { + templateUrl : resourceUrl + '/partials/client-service-account-roles.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); }, + user : function(ClientServiceAccountUserLoader) { + return ClientServiceAccountUserLoader(); + }, + clients : function(ClientListLoader) { + return ClientListLoader(); + }, client : function(ClientLoader) { return ClientLoader(); } }, - controller : 'ClientServiceAccountsCtrl' + controller : 'UserRoleMappingCtrl' }) .when('/create/client/:realm', { templateUrl : resourceUrl + '/partials/client-detail.html', diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 13639198b2b..3fd0fb0ef4d 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1298,25 +1298,5 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv }); -module.controller('ClientServiceAccountsCtrl', function($scope, $http, realm, client, Notifications, Client) { - $scope.realm = realm; - $scope.client = angular.copy(client); - - $scope.serviceAccountsEnabledChanged = function() { - if (client.serviceAccountsEnabled != $scope.client.serviceAccountsEnabled) { - Client.update({ - realm : realm.realm, - client : client.id - }, $scope.client, function() { - $scope.changed = false; - client = angular.copy($scope.client); - Notifications.success("Service Account settings updated."); - }); - } - } - -}); - - diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index f3fe77f4caa..3508bf2c3ac 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -1,4 +1,4 @@ -module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, Notifications, RealmRoleMapping, +module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, clients, client, Notifications, RealmRoleMapping, ClientRoleMapping, AvailableRealmRoleMapping, AvailableClientRoleMapping, CompositeRealmRoleMapping, CompositeClientRoleMapping) { $scope.realm = realm; @@ -7,6 +7,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.selectedRealmMappings = []; $scope.realmMappings = []; $scope.clients = clients; + $scope.client = client; $scope.clientRoles = []; $scope.clientComposite = []; $scope.selectedClientRoles = []; @@ -28,11 +29,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.selectedRealmMappings = []; $scope.selectRealmRoles = []; - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; } @@ -49,11 +50,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); $scope.selectedRealmMappings = []; $scope.selectRealmRoles = []; - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; } @@ -62,11 +63,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl }; $scope.addClientRole = function() { - $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id, + $http.post(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, $scope.selectedClientRoles).success(function() { - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -76,11 +77,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl }; $scope.deleteClientRole = function() { - $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.client.id, + $http.delete(authUrl + '/admin/realms/' + realm.realm + '/users/' + user.id + '/role-mappings/clients/' + $scope.targetClient.id, {data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() { - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); $scope.selectedClientRoles = []; $scope.selectedClientMappings = []; $scope.realmComposite = CompositeRealmRoleMapping.query({realm : realm.realm, userId : user.id}); @@ -92,11 +93,11 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, realm, user, cl $scope.changeClient = function() { console.log('changeClient'); - if ($scope.client) { + if ($scope.targetClient) { console.log('load available'); - $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); - $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.client.id}); + $scope.clientComposite = CompositeClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientRoles = AvailableClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); + $scope.clientMappings = ClientRoleMapping.query({realm : realm.realm, userId : user.id, client : $scope.targetClient.id}); } else { $scope.clientRoles = null; $scope.clientMappings = null; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 773f6f02718..37a95663975 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -282,6 +282,15 @@ module.factory('ClientListLoader', function(Loader, Client, $route, $q) { }); }); +module.factory('ClientServiceAccountUserLoader', function(Loader, ClientServiceAccountUser, $route, $q) { + return Loader.get(ClientServiceAccountUser, function() { + return { + realm : $route.current.params.realm, + client : $route.current.params.client + } + }); +}); + module.factory('RoleMappingLoader', function(Loader, RoleMapping, $route, $q) { var realm = $route.current.params.realm || $route.current.params.client; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 1d6a6be9256..3763ba9a9fb 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -897,6 +897,13 @@ module.factory('ClientOrigins', function($resource) { }); }); +module.factory('ClientServiceAccountUser', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/service-account-user', { + realm : '@realm', + client : '@client' + }); +}); + module.factory('Current', function(Realm, $route, $rootScope) { var current = { realms: {}, diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 49a1997b19e..727c66d5fbf 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -72,6 +72,13 @@ 'Confidential' clients require a secret to initiate login protocol. 'Public' clients do not require a secret. 'Bearer-only' clients are web services that never initiate a login. +
+ + Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. +
+ +
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html new file mode 100644 index 00000000000..03a38d9ac2e --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-account-roles.html @@ -0,0 +1,113 @@ +
+ + + +

{{client.clientId|capitalize}}

+ + + +

{{client.clientId}} Service Accounts

+

+ +
+
+ +
+
+
+ + Realm level roles that can be assigned to service account. + + + +
+
+ + Realm level roles assigned to service account. + + +
+
+ + Assigned realm level roles that may have been inherited from a composite role. + +
+
+
+
+ +
+ + +
+
+
Select client to view roles for client
+
+
+
+ + Client roles available to be assigned. + + +
+
+ + Assigned client roles. + + +
+
+ + Assigned client roles that may have been inherited from a composite role. + +
+
+
+
+ + +
+ Service account is not enabled for {{client.clientId}}. + + +
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html deleted file mode 100644 index 1e5f0e5790a..00000000000 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-service-accounts.html +++ /dev/null @@ -1,28 +0,0 @@ -
- - - -

{{client.clientId|capitalize}}

- - - -

{{client.clientId}} Service Accounts

-

-
-
-
- - Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. -
- -
-
-
- - -
- - \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html index 8223936affc..3228be5c746 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html @@ -52,13 +52,13 @@
-
+
Select client to view roles for client
-
+
Assignable roles from this client. diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html index c82d99f99aa..2aad2d5f12a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html @@ -33,9 +33,9 @@ Helper utility for generating various client adapter configuration formats which you can download or cut and paste to configure your clients. -
  • - Service Accounts - Allows you to authenticate this client to Keycloak and retrieve access tokens dedicated to this client. +
  • + Service Account Roles + Allows you to authenticate role mappings for the service account dedicated to this client.
  • \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java index e2ef2f66fcf..5a8a6e0d65c 100755 --- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_4_0.java @@ -32,7 +32,7 @@ public class MigrateTo1_4_0 { } public void migrateUsers(KeycloakSession session, RealmModel realm) { - List users = session.userStorage().getUsers(realm); + List users = session.userStorage().getUsers(realm, false); for (UserModel user : users) { String email = user.getEmail(); email = KeycloakModelUtils.toLowerCaseSafe(email); diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index 23aaf1bd8db..ee18d798fa8 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -204,8 +204,17 @@ public class UserFederationManager implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, 0, Integer.MAX_VALUE - 1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + UserModel user = session.userStorage().getUserByServiceAccountClient(client); + if (user != null) { + user = validateAndProxyUser(client.getRealm(), user); + } + return user; + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, 0, Integer.MAX_VALUE - 1, includeServiceAccounts); } @@ -242,11 +251,11 @@ public class UserFederationManager implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { + public List getUsers(RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) { return query(new PaginatedQuery() { @Override public List query(RealmModel realm, int first, int max) { - return session.userStorage().getUsers(realm, first, max); + return session.userStorage().getUsers(realm, first, max, includeServiceAccounts); } }, realm, firstResult, maxResults); } diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 19fdad25919..94c2ffcdfdc 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -104,6 +104,9 @@ public interface UserModel { String getFederationLink(); void setFederationLink(String link); + String getServiceAccountClientLink(); + void setServiceAccountClientLink(String clientInternalId); + void addConsent(UserConsentModel consent); UserConsentModel getConsentByClient(String clientInternalId); List getConsents(); diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index f48062f361e..1690b7a70c7 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -25,9 +25,12 @@ public interface UserProvider extends Provider { UserModel getUserByUsername(String username, RealmModel realm); UserModel getUserByEmail(String email, RealmModel realm); UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm); - List getUsers(RealmModel realm); + UserModel getUserByServiceAccountClient(ClientModel client); + List getUsers(RealmModel realm, boolean includeServiceAccounts); + + // Service account is included for counts int getUsersCount(RealmModel realm); - List getUsers(RealmModel realm, int firstResult, int maxResults); + List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); List searchForUser(String search, RealmModel realm); List searchForUser(String search, RealmModel realm, int firstResult, int maxResults); List searchForUserByAttributes(Map attributes, RealmModel realm); diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java index eeae34ffe45..8c82a8e13b8 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java @@ -27,6 +27,7 @@ public class UserEntity extends AbstractIdentifiableEntity { private List credentials = new ArrayList(); private List federatedIdentities; private String federationLink; + private String serviceAccountClientLink; public String getUsername() { return username; @@ -148,5 +149,13 @@ public class UserEntity extends AbstractIdentifiableEntity { public void setFederationLink(String federationLink) { this.federationLink = federationLink; } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.serviceAccountClientLink = serviceAccountClientLink; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index f5261a095ab..cfe08521b04 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -2,6 +2,7 @@ package org.keycloak.models.utils; import org.bouncycastle.openssl.PEMWriter; import org.keycloak.constants.KerberosConstants; +import org.keycloak.constants.ServiceAccountConstants; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -350,6 +351,8 @@ public final class KeycloakModelUtils { return mapperModel; } + // END USER FEDERATION RELATED STUFF + public static String toLowerCaseSafe(String str) { return str==null ? null : str.toLowerCase(); } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a38b305861a..83c8273acf4 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -902,6 +902,14 @@ public class RepresentationToModel { user.addConsent(consentModel); } } + if (userRep.getServiceAccountClientId() != null) { + String clientId = userRep.getServiceAccountClientId(); + ClientModel client = clientMap.get(clientId); + if (client == null) { + throw new RuntimeException("Unable to find client specified for service account link. Client: " + clientId); + } + user.setServiceAccountClientLink(client.getId());; + } return user; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 9599ab93b95..3c1edb37974 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -207,6 +207,16 @@ public class UserModelDelegate implements UserModel { delegate.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return delegate.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + delegate.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { delegate.addConsent(consent); diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index ff152f8f7c6..0bcc37a8b2d 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -107,8 +107,18 @@ public class FileUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + for (UserModel user : inMemoryModel.getUsers(client.getRealm().getId())) { + if (client.getId().equals(user.getServiceAccountClientLink())) { + return user; + } + } + return null; + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override @@ -117,12 +127,27 @@ public class FileUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - List users = new ArrayList(inMemoryModel.getUsers(realm.getId())); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + List users = new ArrayList<>(inMemoryModel.getUsers(realm.getId())); + + if (!includeServiceAccounts) { + users = filterServiceAccountUsers(users); + } + List sortedList = sortedSubList(users, firstResult, maxResults); return sortedList; } + private List filterServiceAccountUsers(List users) { + List result = new ArrayList<>(); + for (UserModel user : users) { + if (user.getServiceAccountClientLink() == null) { + result.add(user); + } + } + return result; + } + protected List sortedSubList(List list, int firstResult, int maxResults) { if (list.isEmpty()) return list; @@ -183,6 +208,9 @@ public class FileUserProvider implements UserProvider { } } + // Remove users with service account link + found = filterServiceAccountUsers(found); + return sortedSubList(found, firstResult, maxResults); } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index 5db8f935063..98e4254ef24 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -477,6 +477,16 @@ public class UserAdapter implements UserModel, Comparable { user.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { // TODO diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java index 4e99e44eb78..aed1394415e 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java @@ -207,8 +207,13 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm) { - return getDelegate().getUsers(realm); + public UserModel getUserByServiceAccountClient(ClientModel client) { + return getDelegate().getUserByServiceAccountClient(client); + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, includeServiceAccounts); } @Override @@ -217,8 +222,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - return getDelegate().getUsers(realm, firstResult, maxResults); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java index 3abe72f7c87..8ed8b6a12b2 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java @@ -74,8 +74,13 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm) { - return getDelegate().getUsers(realm); + public UserModel getUserByServiceAccountClient(ClientModel client) { + return getDelegate().getUserByServiceAccountClient(client); + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, includeServiceAccounts); } @Override @@ -84,8 +89,8 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - return getDelegate().getUsers(realm, firstResult, maxResults); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + return getDelegate().getUsers(realm, firstResult, maxResults, includeServiceAccounts); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java index b075ea118c2..f2b5e33da72 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java @@ -243,6 +243,18 @@ public class UserAdapter implements UserModel { updated.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + if (updated != null) return updated.getServiceAccountClientLink(); + return cached.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + getDelegateForUpdate(); + updated.setServiceAccountClientLink(clientInternalId); + } + @Override public Set getRealmRoleMappings() { if (updated != null) return updated.getRealmRoleMappings(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 3d1395b5833..853677b6d66 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -31,6 +31,7 @@ public class CachedUser implements Serializable { private boolean enabled; private boolean totp; private String federationLink; + private String serviceAccountClientLink; private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); private Set requiredActions = new HashSet<>(); private Set roleMappings = new HashSet(); @@ -49,6 +50,7 @@ public class CachedUser implements Serializable { this.enabled = user.isEnabled(); this.totp = user.isTotp(); this.federationLink = user.getFederationLink(); + this.serviceAccountClientLink = user.getServiceAccountClientLink(); this.requiredActions.addAll(user.getRequiredActions()); for (RoleModel role : user.getRoleMappings()) { roleMappings.add(role.getId()); @@ -114,4 +116,8 @@ public class CachedUser implements Serializable { public String getFederationLink() { return federationLink; } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index ae04a5f6b58..4f02f6c090b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -272,13 +272,29 @@ public class JpaUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public UserModel getUserByServiceAccountClient(ClientModel client) { + TypedQuery query = em.createNamedQuery("getRealmUserByServiceAccount", UserEntity.class); + query.setParameter("realmId", client.getRealm().getId()); + query.setParameter("clientInternalId", client.getId()); + List results = query.getResultList(); + if (results.isEmpty()) { + return null; + } else if (results.size() > 1) { + throw new IllegalStateException("More service account linked users found for client=" + client.getClientId() + + ", results=" + results); + } else { + UserEntity user = results.get(0); + return new UserAdapter(client.getRealm(), em, user); + } + } + + @Override + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override public int getUsersCount(RealmModel realm) { - // TODO: named query? Object count = em.createNamedQuery("getRealmUserCount") .setParameter("realmId", realm.getId()) .getSingleResult(); @@ -286,8 +302,10 @@ public class JpaUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - TypedQuery query = em.createNamedQuery("getAllUsersByRealm", UserEntity.class); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ; + + TypedQuery query = em.createNamedQuery(queryName, UserEntity.class); query.setParameter("realmId", realm.getId()); if (firstResult != -1) { query.setFirstResult(firstResult); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index ca0c284b452..e60377746c9 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -543,6 +543,16 @@ public class UserAdapter implements UserModel { user.setFederationLink(link); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + } + @Override public void addConsent(UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index 5e0769a6b2b..2da16415862 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -21,12 +21,15 @@ import java.util.Collection; */ @NamedQueries({ @NamedQuery(name="getAllUsersByRealm", query="select u from UserEntity u where u.realmId = :realmId order by u.username"), - @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and ( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), + @NamedQuery(name="getAllUsersByRealmExcludeServiceAccount", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) order by u.username"), + @NamedQuery(name="searchForUser", query="select u from UserEntity u where u.realmId = :realmId and (u.serviceAccountClientLink is null) and " + + "( lower(u.username) like :search or lower(concat(u.firstName, ' ', u.lastName)) like :search or u.email like :search ) order by u.username"), @NamedQuery(name="getRealmUserById", query="select u from UserEntity u where u.id = :id and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByUsername", query="select u from UserEntity u where u.username = :username and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByEmail", query="select u from UserEntity u where u.email = :email and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByLastName", query="select u from UserEntity u where u.lastName = :lastName and u.realmId = :realmId"), @NamedQuery(name="getRealmUserByFirstLastName", query="select u from UserEntity u where u.firstName = :first and u.lastName = :last and u.realmId = :realmId"), + @NamedQuery(name="getRealmUserByServiceAccount", query="select u from UserEntity u where u.serviceAccountClientLink = :clientInternalId and u.realmId = :realmId"), @NamedQuery(name="getRealmUserCount", query="select count(u) from UserEntity u where u.realmId = :realmId"), @NamedQuery(name="deleteUsersByRealm", query="delete from UserEntity u where u.realmId = :realmId"), @NamedQuery(name="deleteUsersByRealmAndLink", query="delete from UserEntity u where u.realmId = :realmId and u.federationLink=:link") @@ -77,6 +80,9 @@ public class UserEntity { @Column(name="federation_link") protected String federationLink; + @Column(name="SERVICE_ACCOUNT_CLIENT_LINK") + protected String serviceAccountClientLink; + public String getId() { return id; } @@ -198,6 +204,14 @@ public class UserEntity { this.federationLink = federationLink; } + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.serviceAccountClientLink = serviceAccountClientLink; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index cc720c5f94c..a433fead56c 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -105,6 +105,16 @@ public class MongoUserProvider implements UserProvider { return userEntity == null ? null : new UserAdapter(session, realm, userEntity, invocationContext); } + @Override + public UserModel getUserByServiceAccountClient(ClientModel client) { + DBObject query = new QueryBuilder() + .and("serviceAccountClientLink").is(client.getId()) + .and("realmId").is(client.getRealm().getId()) + .get(); + MongoUserEntity userEntity = getMongoStore().loadSingleEntity(MongoUserEntity.class, query, invocationContext); + return userEntity == null ? null : new UserAdapter(session, client.getRealm(), userEntity, invocationContext); + } + protected List convertUserEntities(RealmModel realm, List userEntities) { List userModels = new ArrayList(); for (MongoUserEntity user : userEntities) { @@ -115,8 +125,8 @@ public class MongoUserProvider implements UserProvider { @Override - public List getUsers(RealmModel realm) { - return getUsers(realm, -1, -1); + public List getUsers(RealmModel realm, boolean includeServiceAccounts) { + return getUsers(realm, -1, -1, includeServiceAccounts); } @Override @@ -128,10 +138,15 @@ public class MongoUserProvider implements UserProvider { } @Override - public List getUsers(RealmModel realm, int firstResult, int maxResults) { - DBObject query = new QueryBuilder() - .and("realmId").is(realm.getId()) - .get(); + public List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + QueryBuilder queryBuilder = new QueryBuilder() + .and("realmId").is(realm.getId()); + + if (!includeServiceAccounts) { + queryBuilder = queryBuilder.and("serviceAccountClientLink").is(null); + } + + DBObject query = queryBuilder.get(); DBObject sort = new BasicDBObject("username", 1); List users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext); return convertUserEntities(realm, users); @@ -170,6 +185,7 @@ public class MongoUserProvider implements UserProvider { QueryBuilder builder = new QueryBuilder().and( new QueryBuilder().and("realmId").is(realm.getId()).get(), + new QueryBuilder().and("serviceAccountClientLink").is(null).get(), new QueryBuilder().or( new QueryBuilder().put("username").regex(caseInsensitivePattern).get(), new QueryBuilder().put("email").regex(caseInsensitivePattern).get(), diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index a8408134d5f..6dae14b371f 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -187,7 +187,7 @@ public class UserAdapter extends AbstractMongoAdapter implement @Override public Map> getAttributes() { - return user.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes()); + return user.getAttributes()==null ? Collections.>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes()); } public MongoUserEntity getUser() { @@ -460,6 +460,17 @@ public class UserAdapter extends AbstractMongoAdapter implement updateUser(); } + @Override + public String getServiceAccountClientLink() { + return user.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + user.setServiceAccountClientLink(clientInternalId); + updateUser(); + } + @Override public void addConsent(UserConsentModel consent) { String clientId = consent.getClient().getId(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java index 3c8b8ad8526..1a8ad0bbc2d 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/ServiceAccountManager.java @@ -106,20 +106,13 @@ public class ServiceAccountManager { protected Response finishClientAuthorization() { event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH); - Map search = new HashMap<>(); - search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); - List users = session.users().searchForUserByUserAttributes(search, realm); + clientUser = session.users().getUserByServiceAccountClient(client); - if (users.size() == 0) { + if (clientUser == null || client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) { // May need to handle bootstrap here as well - logger.warnf("Service account user for client '%s' not found. Creating now", client.getClientId()); + logger.infof("Service account user for client '%s' not found or default protocol mapper for service account not found. Creating now", client.getClientId()); new ClientManager(new RealmManager(session)).enableServiceAccount(client); - users = session.users().searchForUserByUserAttributes(search, realm); - clientUser = users.get(0); - } else if (users.size() == 1) { - clientUser = users.get(0); - } else { - throw new ModelDuplicateException("Multiple service account users found for client '" + client.getClientId() + "' . Check your DB"); + clientUser = session.users().getUserByServiceAccountClient(client); } String clientUsername = clientUser.getUsername(); diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index a7f90795099..1b5a4e88b00 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -51,6 +51,12 @@ public class ClientManager { if (sessions != null) { sessions.onClientRemoved(realm, client); } + + UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client); + if (serviceAccountUser != null) { + realmManager.getSession().users().removeUser(realm, serviceAccountUser); + } + return true; } else { return false; @@ -93,18 +99,15 @@ public class ClientManager { client.setServiceAccountsEnabled(true); // Add dedicated user for this service account - RealmModel realm = client.getRealm(); - Map search = new HashMap<>(); - search.put(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); - List serviceAccountUsers = realmManager.getSession().users().searchForUserByUserAttributes(search, realm); - if (serviceAccountUsers.size() == 0) { + if (realmManager.getSession().users().getUserByServiceAccountClient(client) == null) { String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId(); logger.infof("Creating service account user '%s'", username); - UserModel user = realmManager.getSession().users().addUser(realm, username); + // Don't use federation for service account user + UserModel user = realmManager.getSession().userStorage().addUser(client.getRealm(), username); user.setEnabled(true); user.setEmail(username + "@placeholder.org"); - user.setSingleAttribute(ServiceAccountConstants.SERVICE_ACCOUNT_CLIENT_ATTRIBUTE, client.getId()); + user.setServiceAccountClientLink(client.getId()); } // Add protocol mappers to retrieve clientId in access token diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 67cfb65da84..b85aa39252b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -19,6 +19,7 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; @@ -292,6 +293,31 @@ public class ClientResource { adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success(); } + /** + * Returns user dedicated to this service account + * + * @return + */ + @Path("service-account-user") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public UserRepresentation getServiceAccountUser() { + auth.requireView(); + + UserModel user = session.users().getUserByServiceAccountClient(client); + if (user == null) { + if (client.isServiceAccountsEnabled()) { + new ClientManager(new RealmManager(session)).enableServiceAccount(client); + user = session.users().getUserByServiceAccountClient(client); + } else { + throw new BadRequestException("Service account not enabled for the client '" + client.getClientId() + "'"); + } + } + + return ModelToRepresentation.toRepresentation(user); + } + /** * If the client has an admin URL, push the client's revocation policy to it. * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index 83e6c2fa40e..eeffe5dc22a 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -109,7 +109,7 @@ public class IdentityProviderResource { // Admin changed the ID (alias) of identity provider. We must update all clients and users logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId); - updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm), oldProviderId, newProviderId); + updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId); } adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 83480372171..1a1aa29a5cb 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -554,7 +554,7 @@ public class UsersResource { } userModels = session.users().searchForUserByAttributes(attributes, realm, firstResult, maxResults); } else { - userModels = session.users().getUsers(realm, firstResult, maxResults); + userModels = session.users().getUsers(realm, firstResult, maxResults, false); } for (UserModel user : userModels) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index b883226ed81..8ce5c7ce6b6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -814,7 +814,7 @@ public abstract class AbstractIdentityProviderTest { private void removeTestUsers() { RealmModel realm = getRealm(); - List users = this.session.users().getUsers(realm); + List users = this.session.users().getUsers(realm, true); for (UserModel user : users) { Set identities = this.session.users().getFederatedIdentities(user, realm); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index ee6c5c69f1a..8eb05b64e1f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -288,14 +288,14 @@ public abstract class AbstractKerberosTest { RealmManager manager = new RealmManager(session); RealmModel appRealm = manager.getRealm("test"); - List users = session.userStorage().getUsers(appRealm); + List users = session.userStorage().getUsers(appRealm, true); for (UserModel user : users) { if (!user.getUsername().equals(AssertEvents.DEFAULT_USERNAME)) { session.userStorage().removeUser(appRealm, user); } } - Assert.assertEquals(1, session.userStorage().getUsers(appRealm).size()); + Assert.assertEquals(1, session.userStorage().getUsers(appRealm, true).size()); } finally { keycloakRule.stopSession(session, true); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index e50caf845ca..9b03d5ba06d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -227,7 +227,7 @@ public class SyncProvidersTest { RealmModel testRealm = session.realms().getRealm("test"); // Remove all users from model - for (UserModel user : session.userStorage().getUsers(testRealm)) { + for (UserModel user : session.userStorage().getUsers(testRealm, true)) { session.userStorage().removeUser(testRealm, user); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java index 238184e0f4c..48ed318e374 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java @@ -433,7 +433,7 @@ public class AdapterTest extends AbstractModelTest { RealmModel otherRealm = adapter.createRealm("other"); realmManager.getSession().users().addUser(otherRealm, "bburke"); - Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm).size()); + Assert.assertEquals(1, realmManager.getSession().users().getUsers(otherRealm, false).size()); Assert.assertEquals(1, realmManager.getSession().users().searchForUser("bu", otherRealm).size()); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index 31a9574092d..332c94b56e6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -304,6 +304,14 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(otherAppAdminConsent.isRoleGranted(realm.getRole("admin"))); Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin"))); Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper)); + + // Test service accounts + Assert.assertFalse(application.isServiceAccountsEnabled()); + Assert.assertTrue(otherApp.isServiceAccountsEnabled()); + Assert.assertNull(session.users().getUserByServiceAccountClient(application)); + UserModel linked = session.users().getUserByServiceAccountClient(otherApp); + Assert.assertNotNull(linked); + Assert.assertEquals("my-service-user", linked.getUsername()); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index d0c9d001d13..9455271a098 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -7,6 +7,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.services.managers.ClientManager; import static org.junit.Assert.assertNotNull; @@ -226,6 +227,61 @@ public class UserModelTest extends AbstractModelTest { Assert.assertEquals(0, users.size()); } + @Test + public void testServiceAccountLink() throws Exception { + RealmModel realm = realmManager.createRealm("original"); + ClientModel client = realm.addClient("foo"); + + UserModel user1 = session.users().addUser(realm, "user1"); + user1.setFirstName("John"); + user1.setLastName("Doe"); + + UserModel user2 = session.users().addUser(realm, "user2"); + user2.setFirstName("John"); + user2.setLastName("Doe"); + + // Search + Assert.assertNull(session.users().getUserByServiceAccountClient(client)); + List users = session.users().searchForUser("John Doe", realm); + Assert.assertEquals(2, users.size()); + Assert.assertTrue(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + // Link service account + user1.setServiceAccountClientLink(client.getId()); + + commit(); + + // Search and assert service account user not found + realm = realmManager.getRealmByName("original"); + UserModel searched = session.users().getUserByServiceAccountClient(client); + Assert.assertEquals(searched, user1); + users = session.users().searchForUser("John Doe", realm); + Assert.assertEquals(1, users.size()); + Assert.assertFalse(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + users = session.users().getUsers(realm, false); + Assert.assertEquals(1, users.size()); + Assert.assertFalse(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + users = session.users().getUsers(realm, true); + Assert.assertEquals(2, users.size()); + Assert.assertTrue(users.contains(user1)); + Assert.assertTrue(users.contains(user2)); + + Assert.assertEquals(2, session.users().getUsersCount(realm)); + + // Remove client + new ClientManager(realmManager).removeClient(realm, client); + commit(); + + // Assert service account removed as well + realm = realmManager.getRealmByName("original"); + Assert.assertNull(session.users().getUserByUsername("user1", realm)); + } + public static void assertEquals(UserModel expected, UserModel actual) { Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index 340d9d3c231..9df4385fa20 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -141,6 +141,11 @@ "userName": "mySocialUser@gmail.com" } ] + }, + { + "username": "my-service-user", + "enabled": true, + "serviceAccountClientId": "OtherApp" } ], "clients": [ @@ -158,6 +163,7 @@ "clientId": "OtherApp", "name": "Other Application", "enabled": true, + "serviceAccountsEnabled": true, "protocolMappers" : [ { "name" : "gss delegation credential", From b0cb4572876e88c8521bafbf2c0bf03557f22a86 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 23 Jul 2015 12:17:59 +0200 Subject: [PATCH 06/13] KEYCLOAK-401 Service account docs --- docbook/reference/en/en-US/master.xml | 2 + .../en/en-US/modules/service-accounts.xml | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 docbook/reference/en/en-US/modules/service-accounts.xml diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml index fe6ee0c9cb3..29a49a7bb6b 100755 --- a/docbook/reference/en/en-US/master.xml +++ b/docbook/reference/en/en-US/master.xml @@ -28,6 +28,7 @@ + @@ -122,6 +123,7 @@ This one is short &AccessTypes; &Roles; &DirectAccess; + &ServiceAccounts; &CORS; &Timeouts; &AdminApi; diff --git a/docbook/reference/en/en-US/modules/service-accounts.xml b/docbook/reference/en/en-US/modules/service-accounts.xml new file mode 100644 index 00000000000..e641988a1cc --- /dev/null +++ b/docbook/reference/en/en-US/modules/service-accounts.xml @@ -0,0 +1,56 @@ + + Service Accounts + + Keycloak allows you to obtain an access token dedicated to some Client Application (not to any user). + See Client Credentials Grant + from OAuth 2.0 spec. + + + To use it you must have + registered a valid confidential Client and you need to check the switch Service Accounts Enabled in Keycloak + admin console for this client. In tab Service Account Roles you can configure the roles available to the service account retrieved on behalf of this client. + Don't forget that you need those roles to be available in Scopes of this client as well (unless you have Full Scope Allowed on). + As in normal login, roles from access token are intersection of scopes and the service account roles. + + + + The REST URL to invoke on is /{keycloak-root}/realms/{realm-name}/protocol/openid-connect/token. + Invoking on this URL is a POST request and requires you to post the clientId and clientSecret of the client in Authorization: Basic header. + Later we want to add more mechanisms for authenticating clients. You also need to use parameter grant_type=client_credentials as per OAuth2 specification. + + + For example the POST invocation to retrieve service account can look like this: + + + The response would be this standard JSON document from the OAuth 2.0 specification. + + + + + The retrieved access token can be refreshed or logged out by out-of-bound request. + + + See the example application service-account + from the main Keycloak demo example. + + \ No newline at end of file From 99f74acb94f1761417be3000d8615287866e2807 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Thu, 23 Jul 2015 16:39:27 +0200 Subject: [PATCH 07/13] KEYCLOAK-1542 - rewrote showing info from providers to be generic --- .../DefaultJpaConnectionProviderFactory.java | 48 +-- .../jpa/JpaConnectionProviderFactory.java | 4 +- ...DefaultMongoConnectionFactoryProvider.java | 43 +- .../mongo/MongoConnectionProviderFactory.java | 4 +- .../theme/base/admin/resources/js/app.js | 8 +- .../admin/resources/js/controllers/realm.js | 7 + .../theme/base/admin/resources/js/loaders.js | 4 + .../theme/base/admin/resources/js/services.js | 3 + .../admin/resources/partials/server-info.html | 118 +++--- .../provider/MonitorableProviderFactory.java | 18 - .../provider/ProviderOperationalInfo.java | 14 - .../ServerInfoAwareProviderFactory.java | 20 + services/pom.xml | 15 - .../services/resources/admin/AdminRoot.java | 26 ++ .../admin/ServerInfoAdminResource.java | 254 +----------- .../admin/ServerInfoPageAdminResource.java | 375 ++++++++++++++++++ .../testsuite/admin/AdminAPITest.java | 34 +- 17 files changed, 555 insertions(+), 440 deletions(-) delete mode 100644 model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java delete mode 100644 model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java create mode 100644 model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index aac1cdebd47..a79c82b2286 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -6,20 +6,19 @@ import org.keycloak.Config; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderOperationalInfo; import javax.naming.InitialContext; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.sql.DataSource; - import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; /** @@ -33,7 +32,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide private Config.Scope config; - private DatabaseInfo databaseInfo; + private Map operationalInfo; @Override public JpaConnectionProvider create(KeycloakSession session) { @@ -127,7 +126,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide connection = getConnection(); try{ - prepareDatabaseInfo(connection); + prepareOperationalInfo(connection); if (databaseSchema != null) { logger.trace("Updating database"); @@ -180,16 +179,16 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide } } - protected void prepareDatabaseInfo(Connection connection) { + protected void prepareOperationalInfo(Connection connection) { try { - databaseInfo = new DatabaseInfo(); + operationalInfo = new LinkedHashMap<>(); DatabaseMetaData md = connection.getMetaData(); - databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); - databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); - databaseInfo.databaseUser = md.getUserName(); - databaseInfo.jdbcUrl = md.getURL(); + operationalInfo.put("databaseUrl",md.getURL()); + operationalInfo.put("databaseUser", md.getUserName()); + operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion()); + operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion()); } catch (SQLException e) { - logger.warn("Unable to get database info due " + e.getMessage()); + logger.warn("Unable to prepare operational info due database exception: " + e.getMessage()); } } @@ -209,31 +208,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide } @Override - public DatabaseInfo getOperationalInfo() { - return databaseInfo; - } - - public static class DatabaseInfo implements ProviderOperationalInfo { - protected String jdbcUrl; - protected String databaseUser; - protected String databaseProduct; - protected String databaseDriver; - - public String getJdbcUrl() { - return jdbcUrl; - } - - public String getDatabaseDriver() { - return databaseDriver; - } - - public String getDatabaseUser() { - return databaseUser; - } - - public String getDatabaseProduct() { - return databaseProduct; - } + public Map getOperationalInfo() { + return operationalInfo; } } diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java index ab2b119c29c..288e403335a 100644 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java @@ -1,10 +1,10 @@ package org.keycloak.connections.jpa; -import org.keycloak.provider.MonitorableProviderFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; /** * @author Stian Thorgersen */ -public interface JpaConnectionProviderFactory extends MonitorableProviderFactory { +public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory { } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 2df4b853630..8da15b69c12 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -14,13 +14,13 @@ import org.keycloak.connections.mongo.impl.context.TransactionMongoStoreInvocati import org.keycloak.connections.mongo.updater.MongoUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderOperationalInfo; import javax.net.ssl.SSLSocketFactory; - import java.lang.reflect.Method; import java.net.UnknownHostException; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** * @author Stian Thorgersen @@ -61,7 +61,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro private DB db; protected Config.Scope config; - private MongoDbInfo mongoDbInfo; + private Map operationalInfo; @Override public MongoConnectionProvider create(KeycloakSession session) { @@ -165,11 +165,11 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro client = new MongoClient(new ServerAddress(host, port), clientOptions); } - mongoDbInfo = new MongoDbInfo(); - mongoDbInfo.driverVersion = client.getVersion(); - mongoDbInfo.address = client.getAddress().toString(); - mongoDbInfo.database = dbName; - mongoDbInfo.user = user; + operationalInfo = new LinkedHashMap<>(); + operationalInfo.put("mongoServerAddress", client.getAddress().toString()); + operationalInfo.put("mongoDatabaseName", dbName); + operationalInfo.put("mongoUser", user); + operationalInfo.put("mongoDriverVersion", client.getVersion()); logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); return client; @@ -219,31 +219,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro } @Override - public ProviderOperationalInfo getOperationalInfo() { - return mongoDbInfo; + public Map getOperationalInfo() { + return operationalInfo; } - public static class MongoDbInfo implements ProviderOperationalInfo { - - public String address; - public String database; - public String driverVersion; - public String user; - - public String getAddress() { - return address; - } - - public String getDatabase() { - return database; - } - - public String getDriverVersion() { - return driverVersion; - } - - public String getUser() { - return user; - } - } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java index 3b3a00e5602..bce5fe4b9cf 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java @@ -1,9 +1,9 @@ package org.keycloak.connections.mongo; -import org.keycloak.provider.MonitorableProviderFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; /** * @author Stian Thorgersen */ -public interface MongoConnectionProviderFactory extends MonitorableProviderFactory { +public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory { } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 3f8618bf76d..8797f5ef4b3 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1112,7 +1112,13 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'AuthenticationConfigCreateCtrl' }) .when('/server-info', { - templateUrl : resourceUrl + '/partials/server-info.html' + templateUrl : resourceUrl + '/partials/server-info.html', + resolve : { + serverInfoPage : function(ServerInfoPageLoader) { + return ServerInfoPageLoader(); + } + }, + controller : 'ServerInfoPageCtrl' }) .when('/logout', { templateUrl : resourceUrl + '/partials/home.html', diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 59a3f8518b9..243fff73a7c 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -113,6 +113,13 @@ module.controller('HomeCtrl', function(Realm, Auth, $location) { }); }); +module.controller('ServerInfoPageCtrl', function($scope, ServerInfoPage) { + $scope.serverInfoPage = ServerInfoPage.get(); + $scope.serverInfoPageUpdate = function() { + $scope.serverInfoPage = ServerInfoPage.get(); + }; +}); + module.controller('RealmListCtrl', function($scope, Realm, Current) { $scope.realms = Realm.query(); Current.realms = $scope.realms; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 773f6f02718..329802bc3cf 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -39,6 +39,10 @@ module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) { return Loader.get(ServerInfo); }); +module.factory('ServerInfoPageLoader', function(Loader, ServerInfoPage, $q) { + return Loader.get(ServerInfoPage); +}); + module.factory('RealmLoader', function(Loader, Realm, $route, $q) { return Loader.get(Realm, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 5e9fefda636..7bb14804f89 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -205,6 +205,9 @@ module.factory('ServerInfo', function($resource) { return $resource(authUrl + '/admin/serverinfo'); }); +module.factory('ServerInfoPage', function($resource) { + return $resource(authUrl + '/admin/serverinfopage'); +}); module.factory('ClientProtocolMapper', function($resource) { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index f4e4c6dcf26..0dfac4cb9ac 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -4,15 +4,15 @@
    SPISPI Providers
    - + - + - +
    Keycloak Version{{serverInfo.version}}{{serverInfoPage.version}}
    Server Time{{serverInfo.serverTime}} (update){{serverInfoPage.serverTime}} (update)
    Server Uptime{{serverInfo.serverUptime}}{{serverInfoPage.serverUptime}}
    @@ -22,15 +22,15 @@ - + - + - +
    Total Memory{{serverInfo.memoryInfo.totalFormated}}{{serverInfoPage.memoryInfo.totalFormated}}
    Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%){{serverInfoPage.memoryInfo.freeFormated}} ({{serverInfoPage.memoryInfo.freePercentage}}%)
    Used Memory{{serverInfo.memoryInfo.usedFormated}}{{serverInfoPage.memoryInfo.usedFormated}}
    @@ -42,109 +42,73 @@ - + - + - + - + - + - + - + - + - + - + - + - + - +
    Current Working Directory{{serverInfo.systemInfo.userDir}}{{serverInfoPage.systemInfo.userDir}}
    Java Version{{serverInfo.systemInfo.javaVersion}}{{serverInfoPage.systemInfo.javaVersion}}
    Java Vendor{{serverInfo.systemInfo.javaVendor}}{{serverInfoPage.systemInfo.javaVendor}}
    Java Runtime{{serverInfo.systemInfo.javaRuntime}}{{serverInfoPage.systemInfo.javaRuntime}}
    Java VM{{serverInfo.systemInfo.javaVm}}{{serverInfoPage.systemInfo.javaVm}}
    Java VM Version{{serverInfo.systemInfo.javaVmVersion}}{{serverInfoPage.systemInfo.javaVmVersion}}
    Java Home{{serverInfo.systemInfo.javaHome}}{{serverInfoPage.systemInfo.javaHome}}
    User Name{{serverInfo.systemInfo.userName}}{{serverInfoPage.systemInfo.userName}}
    User Timezone{{serverInfo.systemInfo.userTimezone}}{{serverInfoPage.systemInfo.userTimezone}}
    User Locale{{serverInfo.systemInfo.userLocale}}{{serverInfoPage.systemInfo.userLocale}}
    System Encoding{{serverInfo.systemInfo.fileEncoding}}{{serverInfoPage.systemInfo.fileEncoding}}
    Operating System{{serverInfo.systemInfo.osName}} {{serverInfo.systemInfo.osVersion}}{{serverInfoPage.systemInfo.osName}} {{serverInfoPage.systemInfo.osVersion}}
    OS Architecture{{serverInfo.systemInfo.osArchitecture}}{{serverInfoPage.systemInfo.osArchitecture}}
    -
    +
    Database Info
    - - - - - - - - - - - - - - - + + +
    Database URL{{serverInfo.jpaInfo.jdbcUrl}}
    Database User{{serverInfo.jpaInfo.databaseUser}}
    Database Type{{serverInfo.jpaInfo.databaseProduct}}
    Database Driver{{serverInfo.jpaInfo.databaseDriver}}
    {{key}}{{value}}
    -
    - Mongo DB Info -
    - - - - - - - - - - - - - - - - - -
    Address{{serverInfo.mongoDbInfo.address}}
    Database{{serverInfo.mongoDbInfo.database}}
    User{{serverInfo.mongoDbInfo.user}}
    Driver Version{{serverInfo.mongoDbInfo.driverVersion}}
    -
    -
    -
    Providers @@ -160,11 +124,23 @@ - + {{spi.name}} -
    - {{provider}} +
    + {{provider.name}} + + + + + + + +
    {{key}}{{value}}
    +
    @@ -184,11 +160,23 @@ - + {{spi.name}} -
    - {{provider}} +
    + {{provider.name}} + + + + + + + +
    {{key}}{{value}}
    +
    diff --git a/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java deleted file mode 100644 index 7b9c95c9218..00000000000 --- a/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.keycloak.provider; - -/** - * Provider factory for provider which is monitorable. It means some info about it can be shown on "Server Info" page or accessed over Operational monitoring endpoint. - * - * @author Vlastimil Elias (velias at redhat dot com) - */ -public interface MonitorableProviderFactory extends ProviderFactory { - - /** - * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc). - * Is used to be shown on "Server Info" page or in Operational monitoring endpoint. - * - * @return extendion of {@link ProviderOperationalInfo} - */ - public ProviderOperationalInfo getOperationalInfo(); - -} diff --git a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java deleted file mode 100644 index e37544d2768..00000000000 --- a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.keycloak.provider; - -import java.io.Serializable; - -/** - * Operational info about given Provider. - * Contains info about Provider that can be shown on "Server Info" page or accessed over Operational monitoring endpoint. - * - * @author Vlastimil Elias (velias at redhat dot com) - * @see MonitorableProviderFactory - */ -public interface ProviderOperationalInfo extends Serializable { - -} diff --git a/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java new file mode 100644 index 00000000000..97d17b1dcc0 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java @@ -0,0 +1,20 @@ +package org.keycloak.provider; + +import java.util.Map; + +/** + * Marker interface for ProviderFactory of Provider which wants to show some info on "Server Info" page in Admin console. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public interface ServerInfoAwareProviderFactory extends ProviderFactory { + + /** + * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc) which is + * shown on "Server Info" page. + * + * @return Map with keys describing value and relevant values itself + */ + public Map getOperationalInfo(); + +} diff --git a/services/pom.xml b/services/pom.xml index 16fef06d5cb..da71e9ae6f6 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -39,21 +39,6 @@ keycloak-connections-http-client provided - - org.keycloak - keycloak-connections-jpa - provided - - - org.hibernate - hibernate-entitymanager - provided - - - org.keycloak - keycloak-connections-mongo - provided - org.keycloak keycloak-forms-common-freemarker diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index e1d1fa3c48a..6b3ca3eafa7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -218,6 +218,32 @@ public class AdminRoot { return adminResource; } + /** + * Operational information about the server for "Server Info" page + * + * @param headers + * @return + */ + @Path("serverinfopage") + public ServerInfoPageAdminResource getServerInfoPage(@Context final HttpHeaders headers) { + handlePreflightRequest(); + + AdminAuth auth = authenticateRealmAdminRequest(headers); + if (!isAdmin(auth)) { + throw new ForbiddenException(); + } + + if (auth != null) { + logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); + } + + Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); + + ServerInfoPageAdminResource adminResource = new ServerInfoPageAdminResource(); + ResteasyProviderFactory.getInstance().injectProperties(adminResource); + return adminResource; + } + protected boolean isAdmin(AdminAuth auth) { RealmManager realmManager = new RealmManager(session); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index 3b69304b065..4f1c22575b0 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,11 +1,8 @@ package org.keycloak.services.resources.admin; -import org.jboss.logging.Logger; import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; -import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.connections.mongo.MongoConnectionProvider; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; @@ -19,10 +16,8 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.ProtocolMapper; -import org.keycloak.provider.MonitorableProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; -import org.keycloak.provider.ProviderOperationalInfo; import org.keycloak.provider.Spi; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -31,13 +26,11 @@ import org.keycloak.social.SocialIdentityProvider; import javax.ws.rs.GET; import javax.ws.rs.core.Context; -import java.io.Serializable; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -46,8 +39,6 @@ import java.util.Set; * @author Stian Thorgersen */ public class ServerInfoAdminResource { - - private static final Logger logger = Logger.getLogger(ServerInfoAdminResource.class); private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class); @@ -64,9 +55,6 @@ public class ServerInfoAdminResource { ServerInfoRepresentation info = new ServerInfoRepresentation(); info.version = Version.VERSION; info.serverTime = new Date().toString(); - info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); - info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); - info.systemInfo = (new SystemInfo()).init(); setSocialProviders(info); setIdentityProviders(info); setThemes(info); @@ -77,21 +65,6 @@ public class ServerInfoAdminResource { setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); info.setEnums(ENUMS); - - ProviderFactory jpf = session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); - if(jpf!=null && jpf instanceof MonitorableProviderFactory){ - info.jpaInfo = ((MonitorableProviderFactory)jpf).getOperationalInfo(); - } else { - logger.debug("JPA provider not found or is not monitorable"); - } - - ProviderFactory mpf = session.getKeycloakSessionFactory().getProviderFactory(MongoConnectionProvider.class); - if(mpf!=null && mpf instanceof MonitorableProviderFactory){ - info.mongoDbInfo = ((MonitorableProviderFactory)mpf).getOperationalInfo(); - } else { - logger.debug("Mongo provider not found or is not monitorable"); - } - return info; } @@ -214,170 +187,12 @@ public class ServerInfoAdminResource { info.clientImporters.add(data); } } - - public static class MemoryInfo implements Serializable { - - protected long total; - protected long used; - - public MemoryInfo(){ - } - - /** - * Fill object fwith info. - * @param runtime used to get memory info from. - * @return itself for chaining - */ - public MemoryInfo init(Runtime runtime){ - total = runtime.maxMemory(); - used = runtime.totalMemory() - runtime.freeMemory(); - return this; - } - - public long getTotal(){ - return total; - } - - public String getTotalFormated(){ - return formatMemory(getTotal()); - } - - public long getFree(){ - return getTotal() - getUsed(); - } - public String getFreeFormated(){ - return formatMemory(getFree()); - } - - public long getUsed(){ - return used; - } - - public String getUsedFormated(){ - return formatMemory(getUsed()); - } - - public long getFreePercentage(){ - return getFree() * 100 / getTotal(); - } - - private String formatMemory(long bytes){ - if(bytes > 1024L*1024L){ - return bytes/(1024L *1024L) + " MB"; - } else if(bytes > 1024L){ - return bytes/(1024L) + " kB"; - } else { - return bytes + " B"; - } - } - - } - - public static class SystemInfo implements Serializable { - - protected String javaVersion; - protected String javaVendor; - protected String javaVm; - protected String javaVmVersion; - protected String javaRuntime; - protected String javaHome; - protected String osName; - protected String osArchitecture; - protected String osVersion; - protected String fileEncoding; - protected String userName; - protected String userDir; - protected String userTimezone; - protected String userLocale; - - public SystemInfo() { - } - - /** - * Fill object with info about current system loaded from {@link System} properties. - * @return object itself for chaining - */ - protected SystemInfo init(){ - javaVersion = System.getProperty("java.version"); - javaVendor = System.getProperty("java.vendor"); - javaVm = System.getProperty("java.vm.name"); - javaVmVersion = System.getProperty("java.vm.version"); - javaRuntime = System.getProperty("java.runtime.name"); - javaHome = System.getProperty("java.home"); - osName = System.getProperty("os.name"); - osArchitecture = System.getProperty("os.arch"); - osVersion = System.getProperty("os.version"); - fileEncoding = System.getProperty("file.encoding"); - userName = System.getProperty("user.name"); - userDir = System.getProperty("user.dir"); - userTimezone = System.getProperty("user.timezone"); - userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); - return this; - } - - public String getJavaVersion(){ - return javaVersion; - } - - public String getJavaVendor(){ - return javaVendor; - } - - public String getJavaVm(){ - return javaVm; - } - - public String getJavaVmVersion(){ - return javaVmVersion; - } - - public String getJavaRuntime(){ - return javaRuntime; - } - - public String getJavaHome(){ - return javaHome; - } - - public String getOsName(){ - return osName; - } - - public String getOsArchitecture(){ - return osArchitecture; - } - - public String getOsVersion(){ - return osVersion; - } - - public String getFileEncoding(){ - return fileEncoding; - } - - public String getUserName(){ - return userName; - } - - public String getUserDir(){ - return userDir; - } - - public String getUserTimezone(){ - return userTimezone; - } - - public String getUserLocale(){ - return userLocale; - } - } - - public static class ServerInfoRepresentation implements Serializable { + public static class ServerInfoRepresentation { private String version; + private String serverTime; - private long serverStartupTime; private Map> themes; @@ -393,76 +208,13 @@ public class ServerInfoAdminResource { private Map> builtinProtocolMappers; private Map> enums; - - private MemoryInfo memoryInfo; - private SystemInfo systemInfo; - - private ProviderOperationalInfo jpaInfo; - private ProviderOperationalInfo mongoDbInfo; public ServerInfoRepresentation() { } - - public SystemInfo getSystemInfo(){ - return systemInfo; - } - - public MemoryInfo getMemoryInfo(){ - return memoryInfo; - } - - public ProviderOperationalInfo getJpaInfo() { - return jpaInfo; - } - - public ProviderOperationalInfo getMongoDbInfo() { - return mongoDbInfo; - } public String getServerTime() { return serverTime; } - - public long getServerStartupTime() { - return serverStartupTime; - } - - /** - * @return server startup time formatted - */ - public String getServerStartupTimeFormatted() { - return (new Date(serverStartupTime)).toString(); - } - - /** - * @return server uptime in millis - */ - public long getServerUptimeMillis(){ - return System.currentTimeMillis() - serverStartupTime; - } - - /** - * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" - */ - public String getServerUptime(){ - long diffInSeconds = getServerUptimeMillis()/1000; - long diff[] = new long[] { 0, 0, 0, 0 }; - /* sec */diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); - /* min */diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; - /* hours */diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; - /* days */diff[0] = (diffInSeconds = (diffInSeconds / 24)); - - return String.format( - "%d day%s, %d hour%s, %d minute%s, %d second%s", - diff[0], - diff[0] != 1 ? "s" : "", - diff[1], - diff[1] != 1 ? "s" : "", - diff[2], - diff[2] != 1 ? "s" : "", - diff[3], - diff[3] != 1 ? "s" : ""); - } public String getVersion() { return version; @@ -517,7 +269,7 @@ public class ServerInfoAdminResource { } } - public static class SpiInfoRepresentation implements Serializable { + public static class SpiInfoRepresentation { private String name; private boolean internal; private Set implementations; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java new file mode 100644 index 00000000000..c6a720816cf --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java @@ -0,0 +1,375 @@ +package org.keycloak.services.resources.admin; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; + +import org.jboss.logging.Logger; +import org.keycloak.Version; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ServerInfoAwareProviderFactory; +import org.keycloak.provider.Spi; + +/** + * REST endpoint which return info for "Server Info" page. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class ServerInfoPageAdminResource { + + private static final Logger logger = Logger.getLogger(ServerInfoPageAdminResource.class); + + @Context + private KeycloakSession session; + + /** + * Returns a list of providers and other operational info about the page. + * + * @return + */ + @GET + public ServerInfoRepresentation getInfo() { + ServerInfoRepresentation info = new ServerInfoRepresentation(); + info.version = Version.VERSION; + info.serverTime = new Date().toString(); + info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); + info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); + info.systemInfo = (new SystemInfo()).init(); + setProviders(info); + return info; + } + + private void setProviders(ServerInfoRepresentation info) { + List providers = new LinkedList<>(); + for (Spi spi : ServiceLoader.load(Spi.class)) { + SpiInfoRepresentation spiRep = new SpiInfoRepresentation(); + spiRep.setName(spi.getName()); + spiRep.setInternal(spi.isInternal()); + spiRep.setSystemInfo(ServerInfoAwareProviderFactory.class.isAssignableFrom(spi.getProviderFactoryClass())); + Set s = session.listProviderIds(spi.getProviderClass()); + Set srs = new HashSet<>(); + + if(s!=null){ + for(String name: s){ + SpiImplementationRepresentation sr = new SpiImplementationRepresentation(name); + if(spiRep.isSystemInfo()){ + sr.setOperationalInfo(((ServerInfoAwareProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name)).getOperationalInfo()); + } + srs.add(sr); + } + } + spiRep.setImplementations(srs); + providers.add(spiRep); + } + info.providers = providers; + } + + + public static class MemoryInfo implements Serializable { + + protected long total; + protected long used; + + public MemoryInfo(){ + } + + /** + * Fill object fwith info. + * @param runtime used to get memory info from. + * @return itself for chaining + */ + public MemoryInfo init(Runtime runtime){ + total = runtime.maxMemory(); + used = runtime.totalMemory() - runtime.freeMemory(); + return this; + } + + public long getTotal(){ + return total; + } + + public String getTotalFormated(){ + return formatMemory(getTotal()); + } + + public long getFree(){ + return getTotal() - getUsed(); + } + + public String getFreeFormated(){ + return formatMemory(getFree()); + } + + public long getUsed(){ + return used; + } + + public String getUsedFormated(){ + return formatMemory(getUsed()); + } + + public long getFreePercentage(){ + return getFree() * 100 / getTotal(); + } + + private String formatMemory(long bytes){ + if(bytes > 1024L*1024L){ + return bytes/(1024L *1024L) + " MB"; + } else if(bytes > 1024L){ + return bytes/(1024L) + " kB"; + } else { + return bytes + " B"; + } + } + + } + + public static class SystemInfo implements Serializable { + + protected String javaVersion; + protected String javaVendor; + protected String javaVm; + protected String javaVmVersion; + protected String javaRuntime; + protected String javaHome; + protected String osName; + protected String osArchitecture; + protected String osVersion; + protected String fileEncoding; + protected String userName; + protected String userDir; + protected String userTimezone; + protected String userLocale; + + public SystemInfo() { + } + + /** + * Fill object with info about current system loaded from {@link System} properties. + * @return object itself for chaining + */ + protected SystemInfo init(){ + javaVersion = System.getProperty("java.version"); + javaVendor = System.getProperty("java.vendor"); + javaVm = System.getProperty("java.vm.name"); + javaVmVersion = System.getProperty("java.vm.version"); + javaRuntime = System.getProperty("java.runtime.name"); + javaHome = System.getProperty("java.home"); + osName = System.getProperty("os.name"); + osArchitecture = System.getProperty("os.arch"); + osVersion = System.getProperty("os.version"); + fileEncoding = System.getProperty("file.encoding"); + userName = System.getProperty("user.name"); + userDir = System.getProperty("user.dir"); + userTimezone = System.getProperty("user.timezone"); + userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return this; + } + + public String getJavaVersion(){ + return javaVersion; + } + + public String getJavaVendor(){ + return javaVendor; + } + + public String getJavaVm(){ + return javaVm; + } + + public String getJavaVmVersion(){ + return javaVmVersion; + } + + public String getJavaRuntime(){ + return javaRuntime; + } + + public String getJavaHome(){ + return javaHome; + } + + public String getOsName(){ + return osName; + } + + public String getOsArchitecture(){ + return osArchitecture; + } + + public String getOsVersion(){ + return osVersion; + } + + public String getFileEncoding(){ + return fileEncoding; + } + + public String getUserName(){ + return userName; + } + + public String getUserDir(){ + return userDir; + } + + public String getUserTimezone(){ + return userTimezone; + } + + public String getUserLocale(){ + return userLocale; + } + } + + public static class ServerInfoRepresentation implements Serializable { + + private String version; + private String serverTime; + private long serverStartupTime; + + + private List providers; + + private MemoryInfo memoryInfo; + private SystemInfo systemInfo; + + public ServerInfoRepresentation() { + } + + public SystemInfo getSystemInfo(){ + return systemInfo; + } + + public MemoryInfo getMemoryInfo(){ + return memoryInfo; + } + + public String getServerTime() { + return serverTime; + } + + public long getServerStartupTime() { + return serverStartupTime; + } + + /** + * @return server startup time formatted + */ + public String getServerStartupTimeFormatted() { + return (new Date(serverStartupTime)).toString(); + } + + /** + * @return server uptime in millis + */ + public long getServerUptimeMillis(){ + return System.currentTimeMillis() - serverStartupTime; + } + + /** + * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" + */ + public String getServerUptime(){ + long diffInSeconds = getServerUptimeMillis()/1000; + long diff[] = new long[] { 0, 0, 0, 0 }; + /* sec */diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); + /* min */diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; + /* hours */diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; + /* days */diff[0] = (diffInSeconds = (diffInSeconds / 24)); + + return String.format( + "%d day%s, %d hour%s, %d minute%s, %d second%s", + diff[0], + diff[0] != 1 ? "s" : "", + diff[1], + diff[1] != 1 ? "s" : "", + diff[2], + diff[2] != 1 ? "s" : "", + diff[3], + diff[3] != 1 ? "s" : ""); + } + + public String getVersion() { + return version; + } + + + public List getProviders() { + return providers; + } + } + + public static class SpiInfoRepresentation implements Serializable { + private String name; + private boolean internal; + private boolean systemInfo; + private Set implementations; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isInternal() { + return internal; + } + + public void setInternal(boolean internal) { + this.internal = internal; + } + + public Set getImplementations() { + return implementations; + } + + public boolean isSystemInfo() { + return systemInfo; + } + + public void setSystemInfo(boolean systemInfo) { + this.systemInfo = systemInfo; + } + + public void setImplementations(Set implementations) { + this.implementations = implementations; + } + } + + public static class SpiImplementationRepresentation implements Serializable { + + private String name; + private Map operationalInfo; + + public SpiImplementationRepresentation(String name) { + super(); + this.name = name; + } + + public Map getOperationalInfo() { + return operationalInfo; + } + + public void setOperationalInfo(Map operationalInfo) { + this.operationalInfo = operationalInfo; + } + + public String getName() { + return name; + } + + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 2cca19a0c98..33ead317e41 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -317,13 +317,41 @@ public class AdminAPITest { Assert.assertNotNull(response); Assert.assertEquals(Version.VERSION, response.get("version")); Assert.assertNotNull(response.get("serverTime")); + Assert.assertNotNull(response.get("providers")); + Assert.assertNotNull(response.get("themes")); + Assert.assertNotNull(response.get("enums")); + + // System.out.println(response); + + } + + @Test + public void testServerInfoPage() { + + String token = createToken(); + final String authHeader = "Bearer " + token; + ClientRequestFilter authFilter = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); + } + }; + Client client = ClientBuilder.newBuilder().register(authFilter).build(); + UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth"); + WebTarget target = client.target(AdminRoot.adminBaseUrl(authBase).path("serverinfopage")); + + Map response = target.request().accept("application/json").get(Map.class); + + Assert.assertNotNull(response); + Assert.assertEquals(Version.VERSION, response.get("version")); + Assert.assertNotNull(response.get("serverTime")); + Assert.assertNotNull(response.get("providers")); Assert.assertNotNull(response.get("serverStartupTime")); Assert.assertNotNull(response.get("memoryInfo")); - Assert.assertNotNull(response.get("jpaInfo")); - Assert.assertNull(response.get("mongoDbInfo")); + Assert.assertNotNull(response.get("systemInfo")); - System.out.println(response); + // System.out.println(response); } From dfb871c26a7c6dd0b6b9940f59af66b280f91831 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 20 Jul 2015 17:11:04 +0200 Subject: [PATCH 08/13] KEYCLOAK-1542 - added system and memory info into "System Info" page --- .../admin/resources/partials/server-info.html | 75 +++++++- .../models/KeycloakSessionFactory.java | 2 + .../DefaultKeycloakSessionFactory.java | 12 ++ .../admin/ServerInfoAdminResource.java | 173 ++++++++++++++++-- 4 files changed, 249 insertions(+), 13 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 34c9a845d3a..5590f9576e1 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -8,10 +8,81 @@ Server Time - {{serverInfo.serverTime}} (update) + {{serverInfo.serverTime}} (update) + + + Server Uptime + {{serverInfo.serverUptime}} + + + Current Working Directory + {{serverInfo.systemInfo.userDir}} + + + Java Version + {{serverInfo.systemInfo.javaVersion}} + + + Java Vendor + {{serverInfo.systemInfo.javaVendor}} + + + Java Runtime + {{serverInfo.systemInfo.javaRuntime}} + + + Java VM + {{serverInfo.systemInfo.javaVm}} + + + Java VM Version + {{serverInfo.systemInfo.javaVmVersion}} + + + Java Home + {{serverInfo.systemInfo.javaHome}} + + + User Name + {{serverInfo.systemInfo.userName}} + + + User Timezone + {{serverInfo.systemInfo.userTimezone}} + + + User Locale + {{serverInfo.systemInfo.userLocale}} + + + System Encoding + {{serverInfo.systemInfo.fileEncoding}} + + + Operating System + {{serverInfo.systemInfo.osName}} {{serverInfo.systemInfo.osVersion}} + + + OS Architecture + {{serverInfo.systemInfo.osArchitecture}} - + +

    Java VM Memory Statistics

    + + + + + + + + + + + + + +
    Total Memory{{serverInfo.memoryInfo.totalFormated}}
    Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)
    Used Memory{{serverInfo.memoryInfo.usedFormated}}
    Providers diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java index cbb3da22d5f..82665508c25 100755 --- a/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakSessionFactory.java @@ -18,6 +18,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager { ProviderFactory getProviderFactory(Class clazz, String id); List getProviderFactories(Class clazz); + + long getServerStartupTimestamp(); void close(); } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index e312aa01225..c5be21be10e 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -28,6 +28,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { private Map, Map> factoriesMap = new HashMap, Map>(); protected CopyOnWriteArrayList listeners = new CopyOnWriteArrayList(); + protected long serverStartupTimestamp; + @Override public void register(ProviderEventListener listener) { listeners.add(listener); @@ -46,6 +48,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { } public void init() { + serverStartupTimestamp = System.currentTimeMillis(); + ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers")); for (Spi spi : ServiceLoader.load(Spi.class, getClass().getClassLoader())) { @@ -148,4 +152,12 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory { return factory.getClass().getPackage().getName().startsWith("org.keycloak"); } + /** + * @return timestamp of Keycloak server startup + */ + @Override + public long getServerStartupTimestamp() { + return serverStartupTimestamp; + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index 4f1c22575b0..c600b92e4b4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,5 +1,18 @@ package org.keycloak.services.resources.admin; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; + import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; @@ -24,17 +37,6 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.social.SocialIdentityProvider; -import javax.ws.rs.GET; -import javax.ws.rs.core.Context; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; - /** * @author Stian Thorgersen */ @@ -55,6 +57,7 @@ public class ServerInfoAdminResource { ServerInfoRepresentation info = new ServerInfoRepresentation(); info.version = Version.VERSION; info.serverTime = new Date().toString(); + info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); setSocialProviders(info); setIdentityProviders(info); setThemes(info); @@ -187,12 +190,115 @@ public class ServerInfoAdminResource { info.clientImporters.add(data); } } + + public static class MemoryInfo{ + + public long getTotal(){ + return Runtime.getRuntime().maxMemory(); + } + + public String getTotalFormated(){ + return formatMemory(getTotal()); + } + + public long getFree(){ + return getTotal() - getUsed(); + } + public String getFreeFormated(){ + return formatMemory(getFree()); + } + + public long getUsed(){ + return Runtime.getRuntime().totalMemory(); + } + + public String getUsedFormated(){ + return formatMemory(getUsed()); + } + + public long getFreePercentage(){ + return getFree()*100/getTotal(); + } + + private String formatMemory(long bytes){ + if(bytes > 1024L*1024L){ + return bytes/(1024L *1024L) + " MB"; + } else if(bytes > 1024L){ + return bytes/(1024L) + " kB"; + } else { + return bytes + " B"; + } + } + + } + + public static class SystemInfo { + public String getJavaVersion(){ + return System.getProperty("java.version"); + } + + public String getJavaVendor(){ + return System.getProperty("java.vendor"); + } + + public String getJavaVm(){ + return System.getProperty("java.vm.name"); + } + + public String getJavaVmVersion(){ + return System.getProperty("java.vm.version"); + } + + public String getJavaRuntime(){ + return System.getProperty("java.runtime.name"); + } + + public String getJavaHome(){ + return System.getProperty("java.home"); + } + + public String getOsName(){ + return System.getProperty("os.name"); + } + + public String getOsArchitecture(){ + return System.getProperty("os.arch"); + } + + public String getOsVersion(){ + return System.getProperty("os.version"); + } + + public String getFileEncoding(){ + return System.getProperty("file.encoding"); + } + + public String getUserName(){ + return System.getProperty("user.name"); + } + + public String getUserDir(){ + return System.getProperty("user.dir"); + } + + public String getUserTimezone(){ + return System.getProperty("user.timezone"); + } + + public String getUserLocale(){ + return (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + } + + } + public static class ServerInfoRepresentation { private String version; private String serverTime; + + private long serverStartupTime; private Map> themes; @@ -211,10 +317,55 @@ public class ServerInfoAdminResource { public ServerInfoRepresentation() { } + + public SystemInfo getSystemInfo(){ + return new SystemInfo(); + } + + public MemoryInfo getMemoryInfo(){ + return new MemoryInfo(); + } public String getServerTime() { return serverTime; } + + /** + * @return server startup time formatted + */ + public String getServerStartupTime() { + return (new Date(serverStartupTime)).toString(); + } + + /** + * @return server uptime in millis + */ + public long getServerUptimeMillis(){ + return System.currentTimeMillis() - serverStartupTime; + } + + /** + * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" + */ + public String getServerUptime(){ + long diffInSeconds = getServerUptimeMillis()/1000; + long diff[] = new long[] { 0, 0, 0, 0 }; + /* sec */diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); + /* min */diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; + /* hours */diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; + /* days */diff[0] = (diffInSeconds = (diffInSeconds / 24)); + + return String.format( + "%d day%s, %d hour%s, %d minute%s, %d second%s", + diff[0], + diff[0] != 1 ? "s" : "", + diff[1], + diff[1] != 1 ? "s" : "", + diff[2], + diff[2] != 1 ? "s" : "", + diff[3], + diff[3] != 1 ? "s" : ""); + } public String getVersion() { return version; From 652b2fee8688d817f47f4000c3e63fe13c3a5a6e Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Tue, 21 Jul 2015 16:09:47 +0200 Subject: [PATCH 09/13] KEYCLOAK-1542 - Server Info page extended by info about DB and MongoDB. Functional test for /serverinfo REST endpoint added. --- .../DefaultJpaConnectionProviderFactory.java | 341 ++++++++++------- .../jpa/JpaConnectionProviderFactory.java | 5 +- ...DefaultMongoConnectionFactoryProvider.java | 356 ++++++++++-------- .../mongo/MongoConnectionProviderFactory.java | 4 +- .../admin/resources/partials/server-info.html | 130 +++++-- .../provider/MonitorableProviderFactory.java | 18 + .../provider/ProviderOperationalInfo.java | 22 ++ services/pom.xml | 15 + .../admin/ServerInfoAdminResource.java | 154 ++++++-- .../testsuite/admin/AdminAPITest.java | 63 +++- 10 files changed, 720 insertions(+), 388 deletions(-) create mode 100644 model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java create mode 100644 model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 61bbafa6bf9..36f3c09cce7 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -1,189 +1,242 @@ package org.keycloak.connections.jpa; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import javax.naming.InitialContext; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; + import org.hibernate.ejb.AvailableSettings; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; - -import javax.naming.InitialContext; -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.Persistence; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; +import org.keycloak.provider.ProviderOperationalInfo; /** * @author Stian Thorgersen */ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory { - private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); + private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); - private volatile EntityManagerFactory emf; + private volatile EntityManagerFactory emf; - private Config.Scope config; + private Config.Scope config; - @Override - public JpaConnectionProvider create(KeycloakSession session) { - lazyInit(session); + private DatabaseInfo databaseInfo; - EntityManager em = emf.createEntityManager(); - em = PersistenceExceptionConverter.create(em); - session.getTransaction().enlist(new JpaKeycloakTransaction(em)); - return new DefaultJpaConnectionProvider(em); - } + @Override + public JpaConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public void close() { - if (emf != null) { - emf.close(); - } - } + EntityManager em = emf.createEntityManager(); + em = PersistenceExceptionConverter.create(em); + session.getTransaction().enlist(new JpaKeycloakTransaction(em)); + return new DefaultJpaConnectionProvider(em); + } - @Override - public String getId() { - return "default"; - } + @Override + public void close() { + if (emf != null) { + emf.close(); + } + } - @Override - public void init(Config.Scope config) { - this.config = config; - } + @Override + public String getId() { + return "default"; + } - @Override - public void postInit(KeycloakSessionFactory factory) { + @Override + public void init(Config.Scope config) { + this.config = config; + } - } + @Override + public void postInit(KeycloakSessionFactory factory) { - private void lazyInit(KeycloakSession session) { - if (emf == null) { - synchronized (this) { - if (emf == null) { - logger.debug("Initializing JPA connections"); + } - Connection connection = null; + private void lazyInit(KeycloakSession session) { + if (emf == null) { + synchronized (this) { + if (emf == null) { + logger.debug("Initializing JPA connections"); - String databaseSchema = config.get("databaseSchema"); + Connection connection = null; - Map properties = new HashMap(); + String databaseSchema = config.get("databaseSchema"); - String unitName = "keycloak-default"; + Map properties = new HashMap(); - String dataSource = config.get("dataSource"); - if (dataSource != null) { - if (config.getBoolean("jta", false)) { - properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); - } else { - properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); - } - } else { - properties.put(AvailableSettings.JDBC_URL, config.get("url")); - properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); + String unitName = "keycloak-default"; - String user = config.get("user"); - if (user != null) { - properties.put(AvailableSettings.JDBC_USER, user); - } - String password = config.get("password"); - if (password != null) { - properties.put(AvailableSettings.JDBC_PASSWORD, password); - } - } + String dataSource = config.get("dataSource"); + if (dataSource != null) { + if (config.getBoolean("jta", false)) { + properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); + } else { + properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); + } + } else { + properties.put(AvailableSettings.JDBC_URL, config.get("url")); + properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); - String driverDialect = config.get("driverDialect"); - if (driverDialect != null && driverDialect.length() > 0) { - properties.put("hibernate.dialect", driverDialect); - } + String user = config.get("user"); + if (user != null) { + properties.put(AvailableSettings.JDBC_USER, user); + } + String password = config.get("password"); + if (password != null) { + properties.put(AvailableSettings.JDBC_PASSWORD, password); + } + } - String schema = config.get("schema"); - if (schema != null) { - properties.put("hibernate.default_schema", schema); - } + String driverDialect = config.get("driverDialect"); + if (driverDialect != null && driverDialect.length() > 0) { + properties.put("hibernate.dialect", driverDialect); + } - if (databaseSchema != null) { - if (databaseSchema.equals("development-update")) { - properties.put("hibernate.hbm2ddl.auto", "update"); - databaseSchema = null; - } else if (databaseSchema.equals("development-validate")) { - properties.put("hibernate.hbm2ddl.auto", "validate"); - databaseSchema = null; - } - } + String schema = config.get("schema"); + if (schema != null) { + properties.put("hibernate.default_schema", schema); + } - properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); - properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); + if (databaseSchema != null) { + if (databaseSchema.equals("development-update")) { + properties.put("hibernate.hbm2ddl.auto", "update"); + databaseSchema = null; + } else if (databaseSchema.equals("development-validate")) { + properties.put("hibernate.hbm2ddl.auto", "validate"); + databaseSchema = null; + } + } - if (databaseSchema != null) { - logger.trace("Updating database"); + properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); + properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); - JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); - if (updater == null) { - throw new RuntimeException("Can't update database: JPA updater provider not found"); - } + connection = getConnection(); + prepareDatabaseInfo(connection); - connection = getConnection(); + if (databaseSchema != null) { + logger.trace("Updating database"); - if (databaseSchema.equals("update")) { - String currentVersion = null; - try { - ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); - if (resultSet.next()) { - currentVersion = resultSet.getString(1); - } - } catch (SQLException e) { - } + JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); + if (updater == null) { + throw new RuntimeException("Can't update database: JPA updater provider not found"); + } - if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { - updater.update(session, connection, schema); - } else { - logger.debug("Database is up to date"); - } - } else if (databaseSchema.equals("validate")) { - updater.validate(connection, schema); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } + if (databaseSchema.equals("update")) { + String currentVersion = null; + try { + ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); + if (resultSet.next()) { + currentVersion = resultSet.getString(1); + } + } catch (SQLException e) { + } - logger.trace("Database update completed"); - } + if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { + updater.update(session, connection, schema); + } else { + logger.debug("Database is up to date"); + } + } else if (databaseSchema.equals("validate")) { + updater.validate(connection, schema); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } - logger.trace("Creating EntityManagerFactory"); - emf = Persistence.createEntityManagerFactory(unitName, properties); - logger.trace("EntityManagerFactory created"); + logger.trace("Database update completed"); + } - // Close after creating EntityManagerFactory to prevent in-mem databases from closing - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - logger.warn(e); - } - } - } - } - } - } + logger.trace("Creating EntityManagerFactory"); + emf = Persistence.createEntityManagerFactory(unitName, properties); + logger.trace("EntityManagerFactory created"); - private Connection getConnection() { - try { - String dataSourceLookup = config.get("dataSource"); - if (dataSourceLookup != null) { - DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); - return dataSource.getConnection(); - } else { - Class.forName(config.get("driver")); - return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); - } - } catch (Exception e) { - throw new RuntimeException("Failed to connect to database", e); - } - } + // Close after creating EntityManagerFactory to prevent in-mem databases from closing + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + logger.warn(e); + } + } + } + } + } + } + + protected void prepareDatabaseInfo(Connection connection) { + try { + databaseInfo = new DatabaseInfo(); + DatabaseMetaData md = connection.getMetaData(); + databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); + databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); + databaseInfo.databaseUser = md.getUserName(); + databaseInfo.jdbcUrl = md.getURL(); + } catch (SQLException e) { + logger.warn("Unable to get database info due " + e.getMessage()); + } + } + + private Connection getConnection() { + try { + String dataSourceLookup = config.get("dataSource"); + if (dataSourceLookup != null) { + DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); + return dataSource.getConnection(); + } else { + Class.forName(config.get("driver")); + return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); + } + } catch (Exception e) { + throw new RuntimeException("Failed to connect to database", e); + } + } + + @Override + public DatabaseInfo getOperationalInfo() { + return databaseInfo; + } + + public static class DatabaseInfo implements ProviderOperationalInfo { + protected String jdbcUrl; + protected String databaseUser; + protected String databaseProduct; + protected String databaseDriver; + + public String getJdbcUrl() { + return jdbcUrl; + } + + public String getDatabaseDriver() { + return databaseDriver; + } + + public String getDatabaseUser() { + return databaseUser; + } + + public String getDatabaseProduct() { + return databaseProduct; + } + + @Override + public boolean isOk() { + // TODO KEYCLOAK-1578 - implement operational monitoring of JPA DB connection + return true; + } + } } diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java index 1cf4a5f2026..ab2b119c29c 100644 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java @@ -1,9 +1,10 @@ package org.keycloak.connections.jpa; -import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.MonitorableProviderFactory; /** * @author Stian Thorgersen */ -public interface JpaConnectionProviderFactory extends ProviderFactory { +public interface JpaConnectionProviderFactory extends MonitorableProviderFactory { + } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 36be680a668..c8d1cbaed4a 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -1,10 +1,11 @@ package org.keycloak.connections.mongo; -import com.mongodb.DB; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; +import java.lang.reflect.Method; +import java.net.UnknownHostException; +import java.util.Collections; + +import javax.net.ssl.SSLSocketFactory; + import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.connections.mongo.api.MongoStore; @@ -13,198 +14,225 @@ import org.keycloak.connections.mongo.impl.context.TransactionMongoStoreInvocati import org.keycloak.connections.mongo.updater.MongoUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderOperationalInfo; -import javax.net.ssl.SSLSocketFactory; -import java.lang.reflect.Method; -import java.net.UnknownHostException; -import java.util.Collections; +import com.mongodb.DB; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; /** * @author Stian Thorgersen */ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { - // TODO Make configurable - private String[] entities = new String[]{ - "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", - "org.keycloak.models.entities.IdentityProviderEntity", - "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", - "org.keycloak.models.entities.RequiredCredentialEntity", - "org.keycloak.models.entities.CredentialEntity", - "org.keycloak.models.entities.FederatedIdentityEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", - "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", - "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", - "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", - "org.keycloak.models.entities.UserFederationProviderEntity", - "org.keycloak.models.entities.UserFederationMapperEntity", - "org.keycloak.models.entities.ProtocolMapperEntity", - "org.keycloak.models.entities.IdentityProviderMapperEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", - "org.keycloak.models.entities.AuthenticationExecutionEntity", - "org.keycloak.models.entities.AuthenticationFlowEntity", - "org.keycloak.models.entities.AuthenticatorConfigEntity", - "org.keycloak.models.entities.RequiredActionProviderEntity", - }; + // TODO Make configurable + private String[] entities = new String[] { "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", + "org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.CredentialEntity", + "org.keycloak.models.entities.FederatedIdentityEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", + "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", "org.keycloak.models.entities.UserFederationProviderEntity", + "org.keycloak.models.entities.UserFederationMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity", "org.keycloak.models.entities.IdentityProviderMapperEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.entities.AuthenticationExecutionEntity", + "org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.RequiredActionProviderEntity", }; - private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); + private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); - private volatile MongoClient client; + private volatile MongoClient client; - private MongoStore mongoStore; - private DB db; - protected Config.Scope config; + private MongoStore mongoStore; + private DB db; + protected Config.Scope config; - @Override - public MongoConnectionProvider create(KeycloakSession session) { - lazyInit(session); + private MongoDbInfo mongoDbInfo; - TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); - session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); - return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); - } + @Override + public MongoConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public void init(Config.Scope config) { - this.config = config; - } + TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); + session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); + return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); + } - @Override - public void postInit(KeycloakSessionFactory factory) { + @Override + public void init(Config.Scope config) { + this.config = config; + } - } + @Override + public void postInit(KeycloakSessionFactory factory) { + } - private void lazyInit(KeycloakSession session) { - if (client == null) { - synchronized (this) { - if (client == null) { - try { - this.client = createMongoClient(); + private void lazyInit(KeycloakSession session) { + if (client == null) { + synchronized (this) { + if (client == null) { + try { + this.client = createMongoClient(); - String dbName = config.get("db", "keycloak"); - this.db = client.getDB(dbName); + String dbName = config.get("db", "keycloak"); + this.db = client.getDB(dbName); - String databaseSchema = config.get("databaseSchema"); - if (databaseSchema != null) { - if (databaseSchema.equals("update")) { - MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); + String databaseSchema = config.get("databaseSchema"); + if (databaseSchema != null) { + if (databaseSchema.equals("update")) { + MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); - if (mongoUpdater == null) { - throw new RuntimeException("Can't update database: Mongo updater provider not found"); - } + if (mongoUpdater == null) { + throw new RuntimeException("Can't update database: Mongo updater provider not found"); + } - mongoUpdater.update(session, db); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } - } + mongoUpdater.update(session, db); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } + } - this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - } - } + this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + } - private Class[] getManagedEntities() throws ClassNotFoundException { - Class[] entityClasses = new Class[entities.length]; - for (int i = 0; i < entities.length; i++) { - entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); - } - return entityClasses; - } + private Class[] getManagedEntities() throws ClassNotFoundException { + Class[] entityClasses = new Class[entities.length]; + for (int i = 0; i < entities.length; i++) { + entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); + } + return entityClasses; + } - @Override - public void close() { - if (client != null) { - client.close(); - } - } + @Override + public void close() { + if (client != null) { + client.close(); + } + } - @Override - public String getId() { - return "default"; - } + @Override + public String getId() { + return "default"; + } - /** - * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client - * from different source. - * - * This method can assume that "config" is already set and can use it. - * - * @return mongoClient instance, which will be shared for whole Keycloak - * - * @throws UnknownHostException - */ - protected MongoClient createMongoClient() throws UnknownHostException { - String host = config.get("host", ServerAddress.defaultHost()); - int port = config.getInt("port", ServerAddress.defaultPort()); - String dbName = config.get("db", "keycloak"); + /** + * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo + * client from different source. + * + * This method can assume that "config" is already set and can use it. + * + * @return mongoClient instance, which will be shared for whole Keycloak + * + * @throws UnknownHostException + */ + protected MongoClient createMongoClient() throws UnknownHostException { + String host = config.get("host", ServerAddress.defaultHost()); + int port = config.getInt("port", ServerAddress.defaultPort()); + String dbName = config.get("db", "keycloak"); - String user = config.get("user"); - String password = config.get("password"); + String user = config.get("user"); + String password = config.get("password"); - MongoClientOptions clientOptions = getClientOptions(); + MongoClientOptions clientOptions = getClientOptions(); - MongoClient client; - if (user != null && password != null) { - MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); - client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); - } else { - client = new MongoClient(new ServerAddress(host, port), clientOptions); - } + MongoClient client; + if (user != null && password != null) { + MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); + client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); + } else { + client = new MongoClient(new ServerAddress(host, port), clientOptions); + } - logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); - return client; - } + mongoDbInfo = new MongoDbInfo(); + mongoDbInfo.driverVersion = client.getVersion(); + mongoDbInfo.address = client.getAddress().toString(); + mongoDbInfo.database = dbName; + mongoDbInfo.user = user; - protected MongoClientOptions getClientOptions() { - MongoClientOptions.Builder builder = MongoClientOptions.builder(); - checkIntOption("connectionsPerHost", builder); - checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); - checkIntOption("maxWaitTime", builder); - checkIntOption("connectTimeout", builder); - checkIntOption("socketTimeout", builder); - checkBooleanOption("socketKeepAlive", builder); - checkBooleanOption("autoConnectRetry", builder); - if (config.getLong("maxAutoConnectRetryTime") != null) { - builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); - } - if(config.getBoolean("ssl", false)) { - builder.socketFactory(SSLSocketFactory.getDefault()); - } + logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); + return client; + } - return builder.build(); - } + protected MongoClientOptions getClientOptions() { + MongoClientOptions.Builder builder = MongoClientOptions.builder(); + checkIntOption("connectionsPerHost", builder); + checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); + checkIntOption("maxWaitTime", builder); + checkIntOption("connectTimeout", builder); + checkIntOption("socketTimeout", builder); + checkBooleanOption("socketKeepAlive", builder); + checkBooleanOption("autoConnectRetry", builder); + if (config.getLong("maxAutoConnectRetryTime") != null) { + builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); + } + if (config.getBoolean("ssl", false)) { + builder.socketFactory(SSLSocketFactory.getDefault()); + } - protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { - Boolean val = config.getBoolean(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); - } - } - } + return builder.build(); + } - protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { - Integer val = config.getInt(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); - } - } - } + protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { + Boolean val = config.getBoolean(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); + } + } + } + + protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { + Integer val = config.getInt(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); + } + } + } + + @Override + public ProviderOperationalInfo getOperationalInfo() { + return mongoDbInfo; + } + + public static class MongoDbInfo implements ProviderOperationalInfo { + + public String address; + public String database; + public String driverVersion; + public String user; + + @Override + public boolean isOk() { + // TODO KEYCLOAK-1578 - implement operational monitoring of Mongo DB connection + return true; + } + + public String getAddress() { + return address; + } + + public String getDatabase() { + return database; + } + + public String getDriverVersion() { + return driverVersion; + } + + public String getUser() { + return user; + } + } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java index e787ce63825..3b3a00e5602 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java @@ -1,9 +1,9 @@ package org.keycloak.connections.mongo; -import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.MonitorableProviderFactory; /** * @author Stian Thorgersen */ -public interface MongoConnectionProviderFactory extends ProviderFactory { +public interface MongoConnectionProviderFactory extends MonitorableProviderFactory { } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 5590f9576e1..f4e4c6dcf26 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -1,21 +1,47 @@
    -

    Server Info

    +

    Server Info

    - +
    + + + + + + + + + + + + +
    Keycloak Version{{serverInfo.version}}
    Server Time{{serverInfo.serverTime}} (update)
    Server Uptime{{serverInfo.serverUptime}}
    + +
    + Java VM Memory Statistics +
    + + + + + + + + + + + + + +
    Total Memory{{serverInfo.memoryInfo.totalFormated}}
    Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)
    Used Memory{{serverInfo.memoryInfo.usedFormated}}
    +
    +
    + +
    + System Info +
    + - - - - - - - - - - - - - + @@ -66,23 +92,59 @@ -
    Version{{serverInfo.version}}
    Server Time{{serverInfo.serverTime}} (update)
    Server Uptime{{serverInfo.serverUptime}}
    Current Working DirectoryCurrent Working Directory {{serverInfo.systemInfo.userDir}}
    OS Architecture {{serverInfo.systemInfo.osArchitecture}}
    - -

    Java VM Memory Statistics

    - - - - - - - - - - - - - -
    Total Memory{{serverInfo.memoryInfo.totalFormated}}
    Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)
    Used Memory{{serverInfo.memoryInfo.usedFormated}}
    + +
    +
    + + +
    + Database Info +
    + + + + + + + + + + + + + + + + + +
    Database URL{{serverInfo.jpaInfo.jdbcUrl}}
    Database User{{serverInfo.jpaInfo.databaseUser}}
    Database Type{{serverInfo.jpaInfo.databaseProduct}}
    Database Driver{{serverInfo.jpaInfo.databaseDriver}}
    +
    +
    + +
    + Mongo DB Info +
    + + + + + + + + + + + + + + + + + +
    Address{{serverInfo.mongoDbInfo.address}}
    Database{{serverInfo.mongoDbInfo.database}}
    User{{serverInfo.mongoDbInfo.user}}
    Driver Version{{serverInfo.mongoDbInfo.driverVersion}}
    +
    +
    +
    Providers @@ -93,7 +155,7 @@ - + @@ -117,7 +179,7 @@
    SPISPI Providers
    - + diff --git a/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java new file mode 100644 index 00000000000..7b9c95c9218 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java @@ -0,0 +1,18 @@ +package org.keycloak.provider; + +/** + * Provider factory for provider which is monitorable. It means some info about it can be shown on "Server Info" page or accessed over Operational monitoring endpoint. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public interface MonitorableProviderFactory extends ProviderFactory { + + /** + * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc). + * Is used to be shown on "Server Info" page or in Operational monitoring endpoint. + * + * @return extendion of {@link ProviderOperationalInfo} + */ + public ProviderOperationalInfo getOperationalInfo(); + +} diff --git a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java new file mode 100644 index 00000000000..ca3e8a6849a --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java @@ -0,0 +1,22 @@ +package org.keycloak.provider; + +import java.io.Serializable; + +/** + * Operational info about given Provider. + * Contains info about Provider that can be shown on "Server Info" page or accessed over Operational monitoring endpoint. + * + * @author Vlastimil Elias (velias at redhat dot com) + * @see MonitorableProviderFactory + */ +public interface ProviderOperationalInfo extends Serializable { + + /** + * Return true if provider is OK from operation point of view. It means it is able to perform necessary work. + * It can return false for example if remote DB of JPA provider is not available, or LDAP server of LDAP based user federation provider is not available. + * + * @return true if provider is OK to perform his operation. + */ + boolean isOk(); + +} diff --git a/services/pom.xml b/services/pom.xml index da71e9ae6f6..16fef06d5cb 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -39,6 +39,21 @@ keycloak-connections-http-client provided + + org.keycloak + keycloak-connections-jpa + provided + + + org.hibernate + hibernate-entitymanager + provided + + + org.keycloak + keycloak-connections-mongo + provided + org.keycloak keycloak-forms-common-freemarker diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index c600b92e4b4..91610db0c43 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,5 +1,6 @@ package org.keycloak.services.resources.admin; +import java.io.Serializable; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -13,9 +14,13 @@ import java.util.Set; import javax.ws.rs.GET; import javax.ws.rs.core.Context; +import org.jboss.logging.Logger; import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.connections.mongo.DefaultMongoConnectionFactoryProvider.MongoDbInfo; +import org.keycloak.connections.mongo.MongoConnectionProvider; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; @@ -29,8 +34,10 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.provider.MonitorableProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.ProviderOperationalInfo; import org.keycloak.provider.Spi; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -41,6 +48,8 @@ import org.keycloak.social.SocialIdentityProvider; * @author Stian Thorgersen */ public class ServerInfoAdminResource { + + private static final Logger logger = Logger.getLogger(ServerInfoAdminResource.class); private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class); @@ -58,6 +67,8 @@ public class ServerInfoAdminResource { info.version = Version.VERSION; info.serverTime = new Date().toString(); info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); + info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); + info.systemInfo = (new SystemInfo()).init(); setSocialProviders(info); setIdentityProviders(info); setThemes(info); @@ -68,6 +79,21 @@ public class ServerInfoAdminResource { setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); info.setEnums(ENUMS); + + ProviderFactory jpf = session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); + if(jpf!=null && jpf instanceof MonitorableProviderFactory){ + info.jpaInfo = ((MonitorableProviderFactory)jpf).getOperationalInfo(); + } else { + logger.debug("JPA provider not found or is not monitorable"); + } + + ProviderFactory mpf = session.getKeycloakSessionFactory().getProviderFactory(MongoConnectionProvider.class); + if(mpf!=null && mpf instanceof MonitorableProviderFactory){ + info.mongoDbInfo = ((MonitorableProviderFactory)mpf).getOperationalInfo(); + } else { + logger.debug("Mongo provider not found or is not monitorable"); + } + return info; } @@ -191,10 +217,28 @@ public class ServerInfoAdminResource { } } - public static class MemoryInfo{ + public static class MemoryInfo implements Serializable { + + protected long total; + + protected long used; + + public MemoryInfo(){ + } + + /** + * Fill object fwith info. + * @param runtime used to get memory info from. + * @return itself for chaining + */ + public MemoryInfo init(Runtime runtime){ + total = runtime.maxMemory(); + used = runtime.totalMemory() - runtime.freeMemory(); + return this; + } public long getTotal(){ - return Runtime.getRuntime().maxMemory(); + return total; } public String getTotalFormated(){ @@ -210,7 +254,7 @@ public class ServerInfoAdminResource { } public long getUsed(){ - return Runtime.getRuntime().totalMemory(); + return used; } public String getUsedFormated(){ @@ -218,7 +262,7 @@ public class ServerInfoAdminResource { } public long getFreePercentage(){ - return getFree()*100/getTotal(); + return getFree() * 100 / getTotal(); } private String formatMemory(long bytes){ @@ -233,71 +277,109 @@ public class ServerInfoAdminResource { } - public static class SystemInfo { + public static class SystemInfo implements Serializable { + + protected String javaVersion; + protected String javaVendor; + protected String javaVm; + protected String javaVmVersion; + protected String javaRuntime; + protected String javaHome; + protected String osName; + protected String osArchitecture; + protected String osVersion; + protected String fileEncoding; + protected String userName; + protected String userDir; + protected String userTimezone; + protected String userLocale; + + public SystemInfo() { + } + + /** + * Fill object with info about current system loaded from {@link System} properties. + * @return object itself for chaining + */ + protected SystemInfo init(){ + javaVersion = System.getProperty("java.version"); + javaVendor = System.getProperty("java.vendor"); + javaVm = System.getProperty("java.vm.name"); + javaVmVersion = System.getProperty("java.vm.version"); + javaRuntime = System.getProperty("java.runtime.name"); + javaHome = System.getProperty("java.home"); + osName = System.getProperty("os.name"); + osArchitecture = System.getProperty("os.arch"); + osVersion = System.getProperty("os.version"); + fileEncoding = System.getProperty("file.encoding"); + userName = System.getProperty("user.name"); + userDir = System.getProperty("user.dir"); + userTimezone = System.getProperty("user.timezone"); + userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return this; + } + public String getJavaVersion(){ - return System.getProperty("java.version"); + return javaVersion; } public String getJavaVendor(){ - return System.getProperty("java.vendor"); + return javaVendor; } public String getJavaVm(){ - return System.getProperty("java.vm.name"); + return javaVm; } public String getJavaVmVersion(){ - return System.getProperty("java.vm.version"); + return javaVmVersion; } public String getJavaRuntime(){ - return System.getProperty("java.runtime.name"); + return javaRuntime; } public String getJavaHome(){ - return System.getProperty("java.home"); + return javaHome; } public String getOsName(){ - return System.getProperty("os.name"); + return osName; } public String getOsArchitecture(){ - return System.getProperty("os.arch"); + return osArchitecture; } public String getOsVersion(){ - return System.getProperty("os.version"); + return osVersion; } public String getFileEncoding(){ - return System.getProperty("file.encoding"); + return fileEncoding; } public String getUserName(){ - return System.getProperty("user.name"); + return userName; } public String getUserDir(){ - return System.getProperty("user.dir"); + return userDir; } public String getUserTimezone(){ - return System.getProperty("user.timezone"); + return userTimezone; } public String getUserLocale(){ - return (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return userLocale; } - } - public static class ServerInfoRepresentation { + public static class ServerInfoRepresentation implements Serializable { private String version; - private String serverTime; - private long serverStartupTime; private Map> themes; @@ -314,26 +396,44 @@ public class ServerInfoAdminResource { private Map> builtinProtocolMappers; private Map> enums; + + private MemoryInfo memoryInfo; + private SystemInfo systemInfo; + + private ProviderOperationalInfo jpaInfo; + private ProviderOperationalInfo mongoDbInfo; public ServerInfoRepresentation() { } public SystemInfo getSystemInfo(){ - return new SystemInfo(); + return systemInfo; } public MemoryInfo getMemoryInfo(){ - return new MemoryInfo(); + return memoryInfo; + } + + public ProviderOperationalInfo getJpaInfo() { + return jpaInfo; + } + + public ProviderOperationalInfo getMongoDbInfo() { + return mongoDbInfo; } public String getServerTime() { return serverTime; } + public long getServerStartupTime() { + return serverStartupTime; + } + /** * @return server startup time formatted */ - public String getServerStartupTime() { + public String getServerStartupTimeFormatted() { return (new Date(serverStartupTime)).toString(); } @@ -420,7 +520,7 @@ public class ServerInfoAdminResource { } } - public static class SpiInfoRepresentation { + public static class SpiInfoRepresentation implements Serializable { private String name; private boolean internal; private Set implementations; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 3bde1cb7406..85a81bbbebd 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -21,10 +21,27 @@ */ package org.keycloak.testsuite.admin; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.Config; +import org.keycloak.Version; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -40,22 +57,8 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminRoot; -import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.KeycloakServer; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; /** * Tests Undertow Adapter @@ -295,4 +298,34 @@ public class AdminAPITest { testCreateRealm("/admin-test/testrealm.json"); } + @Test + public void testServerInfo() { + + String token = createToken(); + final String authHeader = "Bearer " + token; + ClientRequestFilter authFilter = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); + } + }; + Client client = ClientBuilder.newBuilder().register(authFilter).build(); + UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth"); + WebTarget target = client.target(AdminRoot.adminBaseUrl(authBase).path("serverinfo")); + + Map response = target.request().accept("application/json").get(Map.class); + + Assert.assertNotNull(response); + Assert.assertEquals(Version.VERSION, response.get("version")); + Assert.assertNotNull(response.get("serverTime")); + Assert.assertNotNull(response.get("serverStartupTime")); + + Assert.assertNotNull(response.get("memoryInfo")); + Assert.assertNotNull(response.get("jpaInfo")); + Assert.assertNull(response.get("mongoDbInfo")); + + System.out.println(response); + + } + } From 79c1d7bafa3eb84bf3927d78d4c394dda3cd6776 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Wed, 22 Jul 2015 13:35:56 +0200 Subject: [PATCH 10/13] KEYCLOAK-1542 - code cleanup ahead of PR --- .../jpa/DefaultJpaConnectionProviderFactory.java | 6 ------ .../mongo/DefaultMongoConnectionFactoryProvider.java | 6 ------ .../org/keycloak/provider/ProviderOperationalInfo.java | 8 -------- .../services/resources/admin/ServerInfoAdminResource.java | 1 - 4 files changed, 21 deletions(-) diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 36f3c09cce7..1795cd13c5f 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -231,12 +231,6 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide public String getDatabaseProduct() { return databaseProduct; } - - @Override - public boolean isOk() { - // TODO KEYCLOAK-1578 - implement operational monitoring of JPA DB connection - return true; - } } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index c8d1cbaed4a..248b880be6c 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -212,12 +212,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro public String driverVersion; public String user; - @Override - public boolean isOk() { - // TODO KEYCLOAK-1578 - implement operational monitoring of Mongo DB connection - return true; - } - public String getAddress() { return address; } diff --git a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java index ca3e8a6849a..e37544d2768 100644 --- a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java +++ b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java @@ -11,12 +11,4 @@ import java.io.Serializable; */ public interface ProviderOperationalInfo extends Serializable { - /** - * Return true if provider is OK from operation point of view. It means it is able to perform necessary work. - * It can return false for example if remote DB of JPA provider is not available, or LDAP server of LDAP based user federation provider is not available. - * - * @return true if provider is OK to perform his operation. - */ - boolean isOk(); - } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index 91610db0c43..dbb0ff0c8f1 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -19,7 +19,6 @@ import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.connections.mongo.DefaultMongoConnectionFactoryProvider.MongoDbInfo; import org.keycloak.connections.mongo.MongoConnectionProvider; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; From 7e41b8df27d4fcbd2e02941fd8ba229a538d71e5 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Wed, 22 Jul 2015 14:28:08 +0200 Subject: [PATCH 11/13] KEYCLOAK-1542 - code formatting cleanup --- .../DefaultJpaConnectionProviderFactory.java | 385 +++++++++--------- ...DefaultMongoConnectionFactoryProvider.java | 377 +++++++++-------- .../admin/ServerInfoAdminResource.java | 28 +- .../testsuite/admin/AdminAPITest.java | 33 +- 4 files changed, 420 insertions(+), 403 deletions(-) diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 1795cd13c5f..aac1cdebd47 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -1,19 +1,5 @@ package org.keycloak.connections.jpa; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; - -import javax.naming.InitialContext; -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.Persistence; -import javax.sql.DataSource; - import org.hibernate.ejb.AvailableSettings; import org.jboss.logging.Logger; import org.keycloak.Config; @@ -22,215 +8,232 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderOperationalInfo; +import javax.naming.InitialContext; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + /** * @author Stian Thorgersen */ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory { - private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); + private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); - private volatile EntityManagerFactory emf; + private volatile EntityManagerFactory emf; - private Config.Scope config; + private Config.Scope config; + + private DatabaseInfo databaseInfo; - private DatabaseInfo databaseInfo; + @Override + public JpaConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public JpaConnectionProvider create(KeycloakSession session) { - lazyInit(session); + EntityManager em = emf.createEntityManager(); + em = PersistenceExceptionConverter.create(em); + session.getTransaction().enlist(new JpaKeycloakTransaction(em)); + return new DefaultJpaConnectionProvider(em); + } - EntityManager em = emf.createEntityManager(); - em = PersistenceExceptionConverter.create(em); - session.getTransaction().enlist(new JpaKeycloakTransaction(em)); - return new DefaultJpaConnectionProvider(em); - } + @Override + public void close() { + if (emf != null) { + emf.close(); + } + } - @Override - public void close() { - if (emf != null) { - emf.close(); - } - } + @Override + public String getId() { + return "default"; + } - @Override - public String getId() { - return "default"; - } + @Override + public void init(Config.Scope config) { + this.config = config; + } - @Override - public void init(Config.Scope config) { - this.config = config; - } + @Override + public void postInit(KeycloakSessionFactory factory) { - @Override - public void postInit(KeycloakSessionFactory factory) { + } - } + private void lazyInit(KeycloakSession session) { + if (emf == null) { + synchronized (this) { + if (emf == null) { + logger.debug("Initializing JPA connections"); - private void lazyInit(KeycloakSession session) { - if (emf == null) { - synchronized (this) { - if (emf == null) { - logger.debug("Initializing JPA connections"); + Connection connection = null; - Connection connection = null; + String databaseSchema = config.get("databaseSchema"); - String databaseSchema = config.get("databaseSchema"); + Map properties = new HashMap(); - Map properties = new HashMap(); + String unitName = "keycloak-default"; - String unitName = "keycloak-default"; + String dataSource = config.get("dataSource"); + if (dataSource != null) { + if (config.getBoolean("jta", false)) { + properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); + } else { + properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); + } + } else { + properties.put(AvailableSettings.JDBC_URL, config.get("url")); + properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); - String dataSource = config.get("dataSource"); - if (dataSource != null) { - if (config.getBoolean("jta", false)) { - properties.put(AvailableSettings.JTA_DATASOURCE, dataSource); - } else { - properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource); - } - } else { - properties.put(AvailableSettings.JDBC_URL, config.get("url")); - properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver")); + String user = config.get("user"); + if (user != null) { + properties.put(AvailableSettings.JDBC_USER, user); + } + String password = config.get("password"); + if (password != null) { + properties.put(AvailableSettings.JDBC_PASSWORD, password); + } + } - String user = config.get("user"); - if (user != null) { - properties.put(AvailableSettings.JDBC_USER, user); - } - String password = config.get("password"); - if (password != null) { - properties.put(AvailableSettings.JDBC_PASSWORD, password); - } - } + String driverDialect = config.get("driverDialect"); + if (driverDialect != null && driverDialect.length() > 0) { + properties.put("hibernate.dialect", driverDialect); + } - String driverDialect = config.get("driverDialect"); - if (driverDialect != null && driverDialect.length() > 0) { - properties.put("hibernate.dialect", driverDialect); - } + String schema = config.get("schema"); + if (schema != null) { + properties.put("hibernate.default_schema", schema); + } - String schema = config.get("schema"); - if (schema != null) { - properties.put("hibernate.default_schema", schema); - } + if (databaseSchema != null) { + if (databaseSchema.equals("development-update")) { + properties.put("hibernate.hbm2ddl.auto", "update"); + databaseSchema = null; + } else if (databaseSchema.equals("development-validate")) { + properties.put("hibernate.hbm2ddl.auto", "validate"); + databaseSchema = null; + } + } - if (databaseSchema != null) { - if (databaseSchema.equals("development-update")) { - properties.put("hibernate.hbm2ddl.auto", "update"); - databaseSchema = null; - } else if (databaseSchema.equals("development-validate")) { - properties.put("hibernate.hbm2ddl.auto", "validate"); - databaseSchema = null; - } - } + properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); + properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); - properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); - properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); + connection = getConnection(); + try{ + prepareDatabaseInfo(connection); + + if (databaseSchema != null) { + logger.trace("Updating database"); + + JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); + if (updater == null) { + throw new RuntimeException("Can't update database: JPA updater provider not found"); + } + + if (databaseSchema.equals("update")) { + String currentVersion = null; + try { + ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); + if (resultSet.next()) { + currentVersion = resultSet.getString(1); + } + } catch (SQLException e) { + } + + if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { + updater.update(session, connection, schema); + } else { + logger.debug("Database is up to date"); + } + } else if (databaseSchema.equals("validate")) { + updater.validate(connection, schema); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } + + logger.trace("Database update completed"); + } + + logger.trace("Creating EntityManagerFactory"); + emf = Persistence.createEntityManagerFactory(unitName, properties); + logger.trace("EntityManagerFactory created"); - connection = getConnection(); - prepareDatabaseInfo(connection); + } finally { + // Close after creating EntityManagerFactory to prevent in-mem databases from closing + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + logger.warn(e); + } + } + } + } + } + } + } + + protected void prepareDatabaseInfo(Connection connection) { + try { + databaseInfo = new DatabaseInfo(); + DatabaseMetaData md = connection.getMetaData(); + databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); + databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); + databaseInfo.databaseUser = md.getUserName(); + databaseInfo.jdbcUrl = md.getURL(); + } catch (SQLException e) { + logger.warn("Unable to get database info due " + e.getMessage()); + } + } - if (databaseSchema != null) { - logger.trace("Updating database"); + private Connection getConnection() { + try { + String dataSourceLookup = config.get("dataSource"); + if (dataSourceLookup != null) { + DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); + return dataSource.getConnection(); + } else { + Class.forName(config.get("driver")); + return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); + } + } catch (Exception e) { + throw new RuntimeException("Failed to connect to database", e); + } + } + + @Override + public DatabaseInfo getOperationalInfo() { + return databaseInfo; + } - JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); - if (updater == null) { - throw new RuntimeException("Can't update database: JPA updater provider not found"); - } + public static class DatabaseInfo implements ProviderOperationalInfo { + protected String jdbcUrl; + protected String databaseUser; + protected String databaseProduct; + protected String databaseDriver; - if (databaseSchema.equals("update")) { - String currentVersion = null; - try { - ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema)); - if (resultSet.next()) { - currentVersion = resultSet.getString(1); - } - } catch (SQLException e) { - } + public String getJdbcUrl() { + return jdbcUrl; + } - if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) { - updater.update(session, connection, schema); - } else { - logger.debug("Database is up to date"); - } - } else if (databaseSchema.equals("validate")) { - updater.validate(connection, schema); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } + public String getDatabaseDriver() { + return databaseDriver; + } - logger.trace("Database update completed"); - } + public String getDatabaseUser() { + return databaseUser; + } - logger.trace("Creating EntityManagerFactory"); - emf = Persistence.createEntityManagerFactory(unitName, properties); - logger.trace("EntityManagerFactory created"); - - // Close after creating EntityManagerFactory to prevent in-mem databases from closing - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - logger.warn(e); - } - } - } - } - } - } - - protected void prepareDatabaseInfo(Connection connection) { - try { - databaseInfo = new DatabaseInfo(); - DatabaseMetaData md = connection.getMetaData(); - databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); - databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); - databaseInfo.databaseUser = md.getUserName(); - databaseInfo.jdbcUrl = md.getURL(); - } catch (SQLException e) { - logger.warn("Unable to get database info due " + e.getMessage()); - } - } - - private Connection getConnection() { - try { - String dataSourceLookup = config.get("dataSource"); - if (dataSourceLookup != null) { - DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); - return dataSource.getConnection(); - } else { - Class.forName(config.get("driver")); - return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password")); - } - } catch (Exception e) { - throw new RuntimeException("Failed to connect to database", e); - } - } - - @Override - public DatabaseInfo getOperationalInfo() { - return databaseInfo; - } - - public static class DatabaseInfo implements ProviderOperationalInfo { - protected String jdbcUrl; - protected String databaseUser; - protected String databaseProduct; - protected String databaseDriver; - - public String getJdbcUrl() { - return jdbcUrl; - } - - public String getDatabaseDriver() { - return databaseDriver; - } - - public String getDatabaseUser() { - return databaseUser; - } - - public String getDatabaseProduct() { - return databaseProduct; - } - } + public String getDatabaseProduct() { + return databaseProduct; + } + } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 248b880be6c..2df4b853630 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -1,10 +1,10 @@ package org.keycloak.connections.mongo; -import java.lang.reflect.Method; -import java.net.UnknownHostException; -import java.util.Collections; - -import javax.net.ssl.SSLSocketFactory; +import com.mongodb.DB; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; import org.jboss.logging.Logger; import org.keycloak.Config; @@ -16,217 +16,234 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderOperationalInfo; -import com.mongodb.DB; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; +import javax.net.ssl.SSLSocketFactory; + +import java.lang.reflect.Method; +import java.net.UnknownHostException; +import java.util.Collections; /** * @author Stian Thorgersen */ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { - // TODO Make configurable - private String[] entities = new String[] { "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", - "org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.CredentialEntity", - "org.keycloak.models.entities.FederatedIdentityEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", - "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", "org.keycloak.models.entities.UserFederationProviderEntity", - "org.keycloak.models.entities.UserFederationMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity", "org.keycloak.models.entities.IdentityProviderMapperEntity", - "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.entities.AuthenticationExecutionEntity", - "org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.RequiredActionProviderEntity", }; + // TODO Make configurable + private String[] entities = new String[]{ + "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", + "org.keycloak.models.entities.IdentityProviderEntity", + "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", + "org.keycloak.models.entities.RequiredCredentialEntity", + "org.keycloak.models.entities.CredentialEntity", + "org.keycloak.models.entities.FederatedIdentityEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", + "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity", + "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", + "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", + "org.keycloak.models.entities.UserFederationProviderEntity", + "org.keycloak.models.entities.UserFederationMapperEntity", + "org.keycloak.models.entities.ProtocolMapperEntity", + "org.keycloak.models.entities.IdentityProviderMapperEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", + "org.keycloak.models.entities.AuthenticationExecutionEntity", + "org.keycloak.models.entities.AuthenticationFlowEntity", + "org.keycloak.models.entities.AuthenticatorConfigEntity", + "org.keycloak.models.entities.RequiredActionProviderEntity", + }; - private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); + private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); - private volatile MongoClient client; + private volatile MongoClient client; - private MongoStore mongoStore; - private DB db; - protected Config.Scope config; + private MongoStore mongoStore; + private DB db; + protected Config.Scope config; + + private MongoDbInfo mongoDbInfo; - private MongoDbInfo mongoDbInfo; + @Override + public MongoConnectionProvider create(KeycloakSession session) { + lazyInit(session); - @Override - public MongoConnectionProvider create(KeycloakSession session) { - lazyInit(session); + TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); + session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); + return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); + } - TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); - session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); - return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); - } + @Override + public void init(Config.Scope config) { + this.config = config; + } - @Override - public void init(Config.Scope config) { - this.config = config; - } + @Override + public void postInit(KeycloakSessionFactory factory) { - @Override - public void postInit(KeycloakSessionFactory factory) { + } - } - private void lazyInit(KeycloakSession session) { - if (client == null) { - synchronized (this) { - if (client == null) { - try { - this.client = createMongoClient(); + private void lazyInit(KeycloakSession session) { + if (client == null) { + synchronized (this) { + if (client == null) { + try { + this.client = createMongoClient(); - String dbName = config.get("db", "keycloak"); - this.db = client.getDB(dbName); + String dbName = config.get("db", "keycloak"); + this.db = client.getDB(dbName); - String databaseSchema = config.get("databaseSchema"); - if (databaseSchema != null) { - if (databaseSchema.equals("update")) { - MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); + String databaseSchema = config.get("databaseSchema"); + if (databaseSchema != null) { + if (databaseSchema.equals("update")) { + MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); - if (mongoUpdater == null) { - throw new RuntimeException("Can't update database: Mongo updater provider not found"); - } + if (mongoUpdater == null) { + throw new RuntimeException("Can't update database: Mongo updater provider not found"); + } - mongoUpdater.update(session, db); - } else { - throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); - } - } + mongoUpdater.update(session, db); + } else { + throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); + } + } - this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - } - } + this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + } - private Class[] getManagedEntities() throws ClassNotFoundException { - Class[] entityClasses = new Class[entities.length]; - for (int i = 0; i < entities.length; i++) { - entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); - } - return entityClasses; - } + private Class[] getManagedEntities() throws ClassNotFoundException { + Class[] entityClasses = new Class[entities.length]; + for (int i = 0; i < entities.length; i++) { + entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); + } + return entityClasses; + } - @Override - public void close() { - if (client != null) { - client.close(); - } - } + @Override + public void close() { + if (client != null) { + client.close(); + } + } - @Override - public String getId() { - return "default"; - } + @Override + public String getId() { + return "default"; + } - /** - * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo - * client from different source. - * - * This method can assume that "config" is already set and can use it. - * - * @return mongoClient instance, which will be shared for whole Keycloak - * - * @throws UnknownHostException - */ - protected MongoClient createMongoClient() throws UnknownHostException { - String host = config.get("host", ServerAddress.defaultHost()); - int port = config.getInt("port", ServerAddress.defaultPort()); - String dbName = config.get("db", "keycloak"); + /** + * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client + * from different source. + * + * This method can assume that "config" is already set and can use it. + * + * @return mongoClient instance, which will be shared for whole Keycloak + * + * @throws UnknownHostException + */ + protected MongoClient createMongoClient() throws UnknownHostException { + String host = config.get("host", ServerAddress.defaultHost()); + int port = config.getInt("port", ServerAddress.defaultPort()); + String dbName = config.get("db", "keycloak"); - String user = config.get("user"); - String password = config.get("password"); + String user = config.get("user"); + String password = config.get("password"); - MongoClientOptions clientOptions = getClientOptions(); + MongoClientOptions clientOptions = getClientOptions(); - MongoClient client; - if (user != null && password != null) { - MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); - client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); - } else { - client = new MongoClient(new ServerAddress(host, port), clientOptions); - } + MongoClient client; + if (user != null && password != null) { + MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); + client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); + } else { + client = new MongoClient(new ServerAddress(host, port), clientOptions); + } + + mongoDbInfo = new MongoDbInfo(); + mongoDbInfo.driverVersion = client.getVersion(); + mongoDbInfo.address = client.getAddress().toString(); + mongoDbInfo.database = dbName; + mongoDbInfo.user = user; + + logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); + return client; + } - mongoDbInfo = new MongoDbInfo(); - mongoDbInfo.driverVersion = client.getVersion(); - mongoDbInfo.address = client.getAddress().toString(); - mongoDbInfo.database = dbName; - mongoDbInfo.user = user; + protected MongoClientOptions getClientOptions() { + MongoClientOptions.Builder builder = MongoClientOptions.builder(); + checkIntOption("connectionsPerHost", builder); + checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); + checkIntOption("maxWaitTime", builder); + checkIntOption("connectTimeout", builder); + checkIntOption("socketTimeout", builder); + checkBooleanOption("socketKeepAlive", builder); + checkBooleanOption("autoConnectRetry", builder); + if (config.getLong("maxAutoConnectRetryTime") != null) { + builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); + } + if(config.getBoolean("ssl", false)) { + builder.socketFactory(SSLSocketFactory.getDefault()); + } - logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); - return client; - } + return builder.build(); + } - protected MongoClientOptions getClientOptions() { - MongoClientOptions.Builder builder = MongoClientOptions.builder(); - checkIntOption("connectionsPerHost", builder); - checkIntOption("threadsAllowedToBlockForConnectionMultiplier", builder); - checkIntOption("maxWaitTime", builder); - checkIntOption("connectTimeout", builder); - checkIntOption("socketTimeout", builder); - checkBooleanOption("socketKeepAlive", builder); - checkBooleanOption("autoConnectRetry", builder); - if (config.getLong("maxAutoConnectRetryTime") != null) { - builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime")); - } - if (config.getBoolean("ssl", false)) { - builder.socketFactory(SSLSocketFactory.getDefault()); - } + protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { + Boolean val = config.getBoolean(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); + } + } + } - return builder.build(); - } + protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { + Integer val = config.getInt(optionName); + if (val != null) { + try { + Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); + m.invoke(builder, val); + } catch (Exception e) { + throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); + } + } + } + + @Override + public ProviderOperationalInfo getOperationalInfo() { + return mongoDbInfo; + } - protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) { - Boolean val = config.getBoolean(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring boolean option " + optionName + " for mongo client. Ensure you used correct value true or false and if this option is supported by mongo driver", e); - } - } - } + public static class MongoDbInfo implements ProviderOperationalInfo { - protected void checkIntOption(String optionName, MongoClientOptions.Builder builder) { - Integer val = config.getInt(optionName); - if (val != null) { - try { - Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); - m.invoke(builder, val); - } catch (Exception e) { - throw new IllegalStateException("Problem configuring int option " + optionName + " for mongo client. Ensure you used correct value (number) and if this option is supported by mongo driver", e); - } - } - } + public String address; + public String database; + public String driverVersion; + public String user; - @Override - public ProviderOperationalInfo getOperationalInfo() { - return mongoDbInfo; - } + public String getAddress() { + return address; + } - public static class MongoDbInfo implements ProviderOperationalInfo { + public String getDatabase() { + return database; + } - public String address; - public String database; - public String driverVersion; - public String user; - - public String getAddress() { - return address; - } - - public String getDatabase() { - return database; - } - - public String getDriverVersion() { - return driverVersion; - } - - public String getUser() { - return user; - } - } + public String getDriverVersion() { + return driverVersion; + } + public String getUser() { + return user; + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index dbb0ff0c8f1..3b69304b065 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,19 +1,5 @@ package org.keycloak.services.resources.admin; -import java.io.Serializable; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; - -import javax.ws.rs.GET; -import javax.ws.rs.core.Context; - import org.jboss.logging.Logger; import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; @@ -43,6 +29,19 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.social.SocialIdentityProvider; +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; +import java.io.Serializable; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + /** * @author Stian Thorgersen */ @@ -219,7 +218,6 @@ public class ServerInfoAdminResource { public static class MemoryInfo implements Serializable { protected long total; - protected long used; public MemoryInfo(){ diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 85a81bbbebd..2cca19a0c98 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -21,22 +21,6 @@ */ package org.keycloak.testsuite.admin; -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.ClientRequestContext; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; - import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; @@ -57,8 +41,23 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminRoot; -import org.keycloak.testsuite.KeycloakServer; import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.KeycloakServer; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Tests Undertow Adapter From ee57c797849edf25504ab83c3ecfea66b894fab9 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Thu, 23 Jul 2015 16:39:27 +0200 Subject: [PATCH 12/13] KEYCLOAK-1542 - rewrote showing info from providers to be generic --- .../DefaultJpaConnectionProviderFactory.java | 48 +-- .../jpa/JpaConnectionProviderFactory.java | 4 +- ...DefaultMongoConnectionFactoryProvider.java | 43 +- .../mongo/MongoConnectionProviderFactory.java | 4 +- .../theme/base/admin/resources/js/app.js | 10 +- .../admin/resources/js/controllers/realm.js | 7 + .../theme/base/admin/resources/js/loaders.js | 4 + .../theme/base/admin/resources/js/services.js | 3 + .../admin/resources/partials/server-info.html | 118 +++--- .../provider/MonitorableProviderFactory.java | 18 - .../provider/ProviderOperationalInfo.java | 14 - .../ServerInfoAwareProviderFactory.java | 20 + services/pom.xml | 15 - .../services/resources/admin/AdminRoot.java | 26 ++ .../admin/ServerInfoAdminResource.java | 254 +----------- .../admin/ServerInfoPageAdminResource.java | 375 ++++++++++++++++++ .../testsuite/admin/AdminAPITest.java | 34 +- 17 files changed, 556 insertions(+), 441 deletions(-) delete mode 100644 model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java delete mode 100644 model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java create mode 100644 model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index aac1cdebd47..a79c82b2286 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -6,20 +6,19 @@ import org.keycloak.Config; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderOperationalInfo; import javax.naming.InitialContext; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.sql.DataSource; - import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; /** @@ -33,7 +32,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide private Config.Scope config; - private DatabaseInfo databaseInfo; + private Map operationalInfo; @Override public JpaConnectionProvider create(KeycloakSession session) { @@ -127,7 +126,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide connection = getConnection(); try{ - prepareDatabaseInfo(connection); + prepareOperationalInfo(connection); if (databaseSchema != null) { logger.trace("Updating database"); @@ -180,16 +179,16 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide } } - protected void prepareDatabaseInfo(Connection connection) { + protected void prepareOperationalInfo(Connection connection) { try { - databaseInfo = new DatabaseInfo(); + operationalInfo = new LinkedHashMap<>(); DatabaseMetaData md = connection.getMetaData(); - databaseInfo.databaseDriver = md.getDriverName() + " " + md.getDriverVersion(); - databaseInfo.databaseProduct = md.getDatabaseProductName() + " " + md.getDatabaseProductVersion(); - databaseInfo.databaseUser = md.getUserName(); - databaseInfo.jdbcUrl = md.getURL(); + operationalInfo.put("databaseUrl",md.getURL()); + operationalInfo.put("databaseUser", md.getUserName()); + operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion()); + operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion()); } catch (SQLException e) { - logger.warn("Unable to get database info due " + e.getMessage()); + logger.warn("Unable to prepare operational info due database exception: " + e.getMessage()); } } @@ -209,31 +208,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide } @Override - public DatabaseInfo getOperationalInfo() { - return databaseInfo; - } - - public static class DatabaseInfo implements ProviderOperationalInfo { - protected String jdbcUrl; - protected String databaseUser; - protected String databaseProduct; - protected String databaseDriver; - - public String getJdbcUrl() { - return jdbcUrl; - } - - public String getDatabaseDriver() { - return databaseDriver; - } - - public String getDatabaseUser() { - return databaseUser; - } - - public String getDatabaseProduct() { - return databaseProduct; - } + public Map getOperationalInfo() { + return operationalInfo; } } diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java index ab2b119c29c..288e403335a 100644 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java @@ -1,10 +1,10 @@ package org.keycloak.connections.jpa; -import org.keycloak.provider.MonitorableProviderFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; /** * @author Stian Thorgersen */ -public interface JpaConnectionProviderFactory extends MonitorableProviderFactory { +public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory { } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 2df4b853630..8da15b69c12 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -14,13 +14,13 @@ import org.keycloak.connections.mongo.impl.context.TransactionMongoStoreInvocati import org.keycloak.connections.mongo.updater.MongoUpdaterProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderOperationalInfo; import javax.net.ssl.SSLSocketFactory; - import java.lang.reflect.Method; import java.net.UnknownHostException; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; /** * @author Stian Thorgersen @@ -61,7 +61,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro private DB db; protected Config.Scope config; - private MongoDbInfo mongoDbInfo; + private Map operationalInfo; @Override public MongoConnectionProvider create(KeycloakSession session) { @@ -165,11 +165,11 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro client = new MongoClient(new ServerAddress(host, port), clientOptions); } - mongoDbInfo = new MongoDbInfo(); - mongoDbInfo.driverVersion = client.getVersion(); - mongoDbInfo.address = client.getAddress().toString(); - mongoDbInfo.database = dbName; - mongoDbInfo.user = user; + operationalInfo = new LinkedHashMap<>(); + operationalInfo.put("mongoServerAddress", client.getAddress().toString()); + operationalInfo.put("mongoDatabaseName", dbName); + operationalInfo.put("mongoUser", user); + operationalInfo.put("mongoDriverVersion", client.getVersion()); logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); return client; @@ -219,31 +219,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro } @Override - public ProviderOperationalInfo getOperationalInfo() { - return mongoDbInfo; + public Map getOperationalInfo() { + return operationalInfo; } - public static class MongoDbInfo implements ProviderOperationalInfo { - - public String address; - public String database; - public String driverVersion; - public String user; - - public String getAddress() { - return address; - } - - public String getDatabase() { - return database; - } - - public String getDriverVersion() { - return driverVersion; - } - - public String getUser() { - return user; - } - } } diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java index 3b3a00e5602..bce5fe4b9cf 100644 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java @@ -1,9 +1,9 @@ package org.keycloak.connections.mongo; -import org.keycloak.provider.MonitorableProviderFactory; +import org.keycloak.provider.ServerInfoAwareProviderFactory; /** * @author Stian Thorgersen */ -public interface MongoConnectionProviderFactory extends MonitorableProviderFactory { +public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory { } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 8fb7c36fed2..4f02ce6b486 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1133,7 +1133,13 @@ module.config([ '$routeProvider', function($routeProvider) { controller : 'AuthenticationConfigCreateCtrl' }) .when('/server-info', { - templateUrl : resourceUrl + '/partials/server-info.html' + templateUrl : resourceUrl + '/partials/server-info.html', + resolve : { + serverInfoPage : function(ServerInfoPageLoader) { + return ServerInfoPageLoader(); + } + }, + controller : 'ServerInfoPageCtrl' }) .when('/logout', { templateUrl : resourceUrl + '/partials/home.html', @@ -1867,4 +1873,4 @@ module.directive( 'kcOpen', function ( $location ) { }); }); }; -}); \ No newline at end of file +}); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 85ad18b5c25..5c5e0232c89 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -125,6 +125,13 @@ module.controller('RealmTabCtrl', function(Dialog, $scope, Current, Realm, Notif }; }); +module.controller('ServerInfoPageCtrl', function($scope, ServerInfoPage) { + $scope.serverInfoPage = ServerInfoPage.get(); + $scope.serverInfoPageUpdate = function() { + $scope.serverInfoPage = ServerInfoPage.get(); + }; +}); + module.controller('RealmListCtrl', function($scope, Realm, Current) { $scope.realms = Realm.query(); Current.realms = $scope.realms; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 37a95663975..2e65b3c7124 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -39,6 +39,10 @@ module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) { return Loader.get(ServerInfo); }); +module.factory('ServerInfoPageLoader', function(Loader, ServerInfoPage, $q) { + return Loader.get(ServerInfoPage); +}); + module.factory('RealmLoader', function(Loader, Realm, $route, $q) { return Loader.get(Realm, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 3763ba9a9fb..b5d8c330f3d 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -219,6 +219,9 @@ module.factory('ServerInfo', function($resource) { return $resource(authUrl + '/admin/serverinfo'); }); +module.factory('ServerInfoPage', function($resource) { + return $resource(authUrl + '/admin/serverinfopage'); +}); module.factory('ClientProtocolMapper', function($resource) { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index f4e4c6dcf26..0dfac4cb9ac 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -4,15 +4,15 @@
    SPISPI Providers
    - + - + - +
    Keycloak Version{{serverInfo.version}}{{serverInfoPage.version}}
    Server Time{{serverInfo.serverTime}} (update){{serverInfoPage.serverTime}} (update)
    Server Uptime{{serverInfo.serverUptime}}{{serverInfoPage.serverUptime}}
    @@ -22,15 +22,15 @@ - + - + - +
    Total Memory{{serverInfo.memoryInfo.totalFormated}}{{serverInfoPage.memoryInfo.totalFormated}}
    Free Memory{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%){{serverInfoPage.memoryInfo.freeFormated}} ({{serverInfoPage.memoryInfo.freePercentage}}%)
    Used Memory{{serverInfo.memoryInfo.usedFormated}}{{serverInfoPage.memoryInfo.usedFormated}}
    @@ -42,109 +42,73 @@ - + - + - + - + - + - + - + - + - + - + - + - + - +
    Current Working Directory{{serverInfo.systemInfo.userDir}}{{serverInfoPage.systemInfo.userDir}}
    Java Version{{serverInfo.systemInfo.javaVersion}}{{serverInfoPage.systemInfo.javaVersion}}
    Java Vendor{{serverInfo.systemInfo.javaVendor}}{{serverInfoPage.systemInfo.javaVendor}}
    Java Runtime{{serverInfo.systemInfo.javaRuntime}}{{serverInfoPage.systemInfo.javaRuntime}}
    Java VM{{serverInfo.systemInfo.javaVm}}{{serverInfoPage.systemInfo.javaVm}}
    Java VM Version{{serverInfo.systemInfo.javaVmVersion}}{{serverInfoPage.systemInfo.javaVmVersion}}
    Java Home{{serverInfo.systemInfo.javaHome}}{{serverInfoPage.systemInfo.javaHome}}
    User Name{{serverInfo.systemInfo.userName}}{{serverInfoPage.systemInfo.userName}}
    User Timezone{{serverInfo.systemInfo.userTimezone}}{{serverInfoPage.systemInfo.userTimezone}}
    User Locale{{serverInfo.systemInfo.userLocale}}{{serverInfoPage.systemInfo.userLocale}}
    System Encoding{{serverInfo.systemInfo.fileEncoding}}{{serverInfoPage.systemInfo.fileEncoding}}
    Operating System{{serverInfo.systemInfo.osName}} {{serverInfo.systemInfo.osVersion}}{{serverInfoPage.systemInfo.osName}} {{serverInfoPage.systemInfo.osVersion}}
    OS Architecture{{serverInfo.systemInfo.osArchitecture}}{{serverInfoPage.systemInfo.osArchitecture}}
    -
    +
    Database Info
    - - - - - - - - - - - - - - - + + +
    Database URL{{serverInfo.jpaInfo.jdbcUrl}}
    Database User{{serverInfo.jpaInfo.databaseUser}}
    Database Type{{serverInfo.jpaInfo.databaseProduct}}
    Database Driver{{serverInfo.jpaInfo.databaseDriver}}
    {{key}}{{value}}
    -
    - Mongo DB Info -
    - - - - - - - - - - - - - - - - - -
    Address{{serverInfo.mongoDbInfo.address}}
    Database{{serverInfo.mongoDbInfo.database}}
    User{{serverInfo.mongoDbInfo.user}}
    Driver Version{{serverInfo.mongoDbInfo.driverVersion}}
    -
    -
    -
    Providers @@ -160,11 +124,23 @@ - + {{spi.name}} -
    - {{provider}} +
    + {{provider.name}} + + + + + + + +
    {{key}}{{value}}
    +
    @@ -184,11 +160,23 @@ - + {{spi.name}} -
    - {{provider}} +
    + {{provider.name}} + + + + + + + +
    {{key}}{{value}}
    +
    diff --git a/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java deleted file mode 100644 index 7b9c95c9218..00000000000 --- a/model/api/src/main/java/org/keycloak/provider/MonitorableProviderFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.keycloak.provider; - -/** - * Provider factory for provider which is monitorable. It means some info about it can be shown on "Server Info" page or accessed over Operational monitoring endpoint. - * - * @author Vlastimil Elias (velias at redhat dot com) - */ -public interface MonitorableProviderFactory extends ProviderFactory { - - /** - * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc). - * Is used to be shown on "Server Info" page or in Operational monitoring endpoint. - * - * @return extendion of {@link ProviderOperationalInfo} - */ - public ProviderOperationalInfo getOperationalInfo(); - -} diff --git a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java b/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java deleted file mode 100644 index e37544d2768..00000000000 --- a/model/api/src/main/java/org/keycloak/provider/ProviderOperationalInfo.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.keycloak.provider; - -import java.io.Serializable; - -/** - * Operational info about given Provider. - * Contains info about Provider that can be shown on "Server Info" page or accessed over Operational monitoring endpoint. - * - * @author Vlastimil Elias (velias at redhat dot com) - * @see MonitorableProviderFactory - */ -public interface ProviderOperationalInfo extends Serializable { - -} diff --git a/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java b/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java new file mode 100644 index 00000000000..97d17b1dcc0 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/provider/ServerInfoAwareProviderFactory.java @@ -0,0 +1,20 @@ +package org.keycloak.provider; + +import java.util.Map; + +/** + * Marker interface for ProviderFactory of Provider which wants to show some info on "Server Info" page in Admin console. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public interface ServerInfoAwareProviderFactory extends ProviderFactory { + + /** + * Get operational info about given provider. This info contains informations about providers configuration and operational conditions (eg. errors in connection to remote systems etc) which is + * shown on "Server Info" page. + * + * @return Map with keys describing value and relevant values itself + */ + public Map getOperationalInfo(); + +} diff --git a/services/pom.xml b/services/pom.xml index 16fef06d5cb..da71e9ae6f6 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -39,21 +39,6 @@ keycloak-connections-http-client provided - - org.keycloak - keycloak-connections-jpa - provided - - - org.hibernate - hibernate-entitymanager - provided - - - org.keycloak - keycloak-connections-mongo - provided - org.keycloak keycloak-forms-common-freemarker diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index e1d1fa3c48a..6b3ca3eafa7 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -218,6 +218,32 @@ public class AdminRoot { return adminResource; } + /** + * Operational information about the server for "Server Info" page + * + * @param headers + * @return + */ + @Path("serverinfopage") + public ServerInfoPageAdminResource getServerInfoPage(@Context final HttpHeaders headers) { + handlePreflightRequest(); + + AdminAuth auth = authenticateRealmAdminRequest(headers); + if (!isAdmin(auth)) { + throw new ForbiddenException(); + } + + if (auth != null) { + logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); + } + + Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); + + ServerInfoPageAdminResource adminResource = new ServerInfoPageAdminResource(); + ResteasyProviderFactory.getInstance().injectProperties(adminResource); + return adminResource; + } + protected boolean isAdmin(AdminAuth auth) { RealmManager realmManager = new RealmManager(session); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java index 3b69304b065..4f1c22575b0 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java @@ -1,11 +1,8 @@ package org.keycloak.services.resources.admin; -import org.jboss.logging.Logger; import org.keycloak.Version; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; -import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.connections.mongo.MongoConnectionProvider; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; @@ -19,10 +16,8 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.ProtocolMapper; -import org.keycloak.provider.MonitorableProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderFactory; -import org.keycloak.provider.ProviderOperationalInfo; import org.keycloak.provider.Spi; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -31,13 +26,11 @@ import org.keycloak.social.SocialIdentityProvider; import javax.ws.rs.GET; import javax.ws.rs.core.Context; -import java.io.Serializable; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -46,8 +39,6 @@ import java.util.Set; * @author Stian Thorgersen */ public class ServerInfoAdminResource { - - private static final Logger logger = Logger.getLogger(ServerInfoAdminResource.class); private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class); @@ -64,9 +55,6 @@ public class ServerInfoAdminResource { ServerInfoRepresentation info = new ServerInfoRepresentation(); info.version = Version.VERSION; info.serverTime = new Date().toString(); - info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); - info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); - info.systemInfo = (new SystemInfo()).init(); setSocialProviders(info); setIdentityProviders(info); setThemes(info); @@ -77,21 +65,6 @@ public class ServerInfoAdminResource { setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); info.setEnums(ENUMS); - - ProviderFactory jpf = session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); - if(jpf!=null && jpf instanceof MonitorableProviderFactory){ - info.jpaInfo = ((MonitorableProviderFactory)jpf).getOperationalInfo(); - } else { - logger.debug("JPA provider not found or is not monitorable"); - } - - ProviderFactory mpf = session.getKeycloakSessionFactory().getProviderFactory(MongoConnectionProvider.class); - if(mpf!=null && mpf instanceof MonitorableProviderFactory){ - info.mongoDbInfo = ((MonitorableProviderFactory)mpf).getOperationalInfo(); - } else { - logger.debug("Mongo provider not found or is not monitorable"); - } - return info; } @@ -214,170 +187,12 @@ public class ServerInfoAdminResource { info.clientImporters.add(data); } } - - public static class MemoryInfo implements Serializable { - - protected long total; - protected long used; - - public MemoryInfo(){ - } - - /** - * Fill object fwith info. - * @param runtime used to get memory info from. - * @return itself for chaining - */ - public MemoryInfo init(Runtime runtime){ - total = runtime.maxMemory(); - used = runtime.totalMemory() - runtime.freeMemory(); - return this; - } - - public long getTotal(){ - return total; - } - - public String getTotalFormated(){ - return formatMemory(getTotal()); - } - - public long getFree(){ - return getTotal() - getUsed(); - } - public String getFreeFormated(){ - return formatMemory(getFree()); - } - - public long getUsed(){ - return used; - } - - public String getUsedFormated(){ - return formatMemory(getUsed()); - } - - public long getFreePercentage(){ - return getFree() * 100 / getTotal(); - } - - private String formatMemory(long bytes){ - if(bytes > 1024L*1024L){ - return bytes/(1024L *1024L) + " MB"; - } else if(bytes > 1024L){ - return bytes/(1024L) + " kB"; - } else { - return bytes + " B"; - } - } - - } - - public static class SystemInfo implements Serializable { - - protected String javaVersion; - protected String javaVendor; - protected String javaVm; - protected String javaVmVersion; - protected String javaRuntime; - protected String javaHome; - protected String osName; - protected String osArchitecture; - protected String osVersion; - protected String fileEncoding; - protected String userName; - protected String userDir; - protected String userTimezone; - protected String userLocale; - - public SystemInfo() { - } - - /** - * Fill object with info about current system loaded from {@link System} properties. - * @return object itself for chaining - */ - protected SystemInfo init(){ - javaVersion = System.getProperty("java.version"); - javaVendor = System.getProperty("java.vendor"); - javaVm = System.getProperty("java.vm.name"); - javaVmVersion = System.getProperty("java.vm.version"); - javaRuntime = System.getProperty("java.runtime.name"); - javaHome = System.getProperty("java.home"); - osName = System.getProperty("os.name"); - osArchitecture = System.getProperty("os.arch"); - osVersion = System.getProperty("os.version"); - fileEncoding = System.getProperty("file.encoding"); - userName = System.getProperty("user.name"); - userDir = System.getProperty("user.dir"); - userTimezone = System.getProperty("user.timezone"); - userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); - return this; - } - - public String getJavaVersion(){ - return javaVersion; - } - - public String getJavaVendor(){ - return javaVendor; - } - - public String getJavaVm(){ - return javaVm; - } - - public String getJavaVmVersion(){ - return javaVmVersion; - } - - public String getJavaRuntime(){ - return javaRuntime; - } - - public String getJavaHome(){ - return javaHome; - } - - public String getOsName(){ - return osName; - } - - public String getOsArchitecture(){ - return osArchitecture; - } - - public String getOsVersion(){ - return osVersion; - } - - public String getFileEncoding(){ - return fileEncoding; - } - - public String getUserName(){ - return userName; - } - - public String getUserDir(){ - return userDir; - } - - public String getUserTimezone(){ - return userTimezone; - } - - public String getUserLocale(){ - return userLocale; - } - } - - public static class ServerInfoRepresentation implements Serializable { + public static class ServerInfoRepresentation { private String version; + private String serverTime; - private long serverStartupTime; private Map> themes; @@ -393,76 +208,13 @@ public class ServerInfoAdminResource { private Map> builtinProtocolMappers; private Map> enums; - - private MemoryInfo memoryInfo; - private SystemInfo systemInfo; - - private ProviderOperationalInfo jpaInfo; - private ProviderOperationalInfo mongoDbInfo; public ServerInfoRepresentation() { } - - public SystemInfo getSystemInfo(){ - return systemInfo; - } - - public MemoryInfo getMemoryInfo(){ - return memoryInfo; - } - - public ProviderOperationalInfo getJpaInfo() { - return jpaInfo; - } - - public ProviderOperationalInfo getMongoDbInfo() { - return mongoDbInfo; - } public String getServerTime() { return serverTime; } - - public long getServerStartupTime() { - return serverStartupTime; - } - - /** - * @return server startup time formatted - */ - public String getServerStartupTimeFormatted() { - return (new Date(serverStartupTime)).toString(); - } - - /** - * @return server uptime in millis - */ - public long getServerUptimeMillis(){ - return System.currentTimeMillis() - serverStartupTime; - } - - /** - * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" - */ - public String getServerUptime(){ - long diffInSeconds = getServerUptimeMillis()/1000; - long diff[] = new long[] { 0, 0, 0, 0 }; - /* sec */diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); - /* min */diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; - /* hours */diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; - /* days */diff[0] = (diffInSeconds = (diffInSeconds / 24)); - - return String.format( - "%d day%s, %d hour%s, %d minute%s, %d second%s", - diff[0], - diff[0] != 1 ? "s" : "", - diff[1], - diff[1] != 1 ? "s" : "", - diff[2], - diff[2] != 1 ? "s" : "", - diff[3], - diff[3] != 1 ? "s" : ""); - } public String getVersion() { return version; @@ -517,7 +269,7 @@ public class ServerInfoAdminResource { } } - public static class SpiInfoRepresentation implements Serializable { + public static class SpiInfoRepresentation { private String name; private boolean internal; private Set implementations; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java new file mode 100644 index 00000000000..c6a720816cf --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoPageAdminResource.java @@ -0,0 +1,375 @@ +package org.keycloak.services.resources.admin; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; + +import org.jboss.logging.Logger; +import org.keycloak.Version; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ServerInfoAwareProviderFactory; +import org.keycloak.provider.Spi; + +/** + * REST endpoint which return info for "Server Info" page. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class ServerInfoPageAdminResource { + + private static final Logger logger = Logger.getLogger(ServerInfoPageAdminResource.class); + + @Context + private KeycloakSession session; + + /** + * Returns a list of providers and other operational info about the page. + * + * @return + */ + @GET + public ServerInfoRepresentation getInfo() { + ServerInfoRepresentation info = new ServerInfoRepresentation(); + info.version = Version.VERSION; + info.serverTime = new Date().toString(); + info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); + info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime()); + info.systemInfo = (new SystemInfo()).init(); + setProviders(info); + return info; + } + + private void setProviders(ServerInfoRepresentation info) { + List providers = new LinkedList<>(); + for (Spi spi : ServiceLoader.load(Spi.class)) { + SpiInfoRepresentation spiRep = new SpiInfoRepresentation(); + spiRep.setName(spi.getName()); + spiRep.setInternal(spi.isInternal()); + spiRep.setSystemInfo(ServerInfoAwareProviderFactory.class.isAssignableFrom(spi.getProviderFactoryClass())); + Set s = session.listProviderIds(spi.getProviderClass()); + Set srs = new HashSet<>(); + + if(s!=null){ + for(String name: s){ + SpiImplementationRepresentation sr = new SpiImplementationRepresentation(name); + if(spiRep.isSystemInfo()){ + sr.setOperationalInfo(((ServerInfoAwareProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name)).getOperationalInfo()); + } + srs.add(sr); + } + } + spiRep.setImplementations(srs); + providers.add(spiRep); + } + info.providers = providers; + } + + + public static class MemoryInfo implements Serializable { + + protected long total; + protected long used; + + public MemoryInfo(){ + } + + /** + * Fill object fwith info. + * @param runtime used to get memory info from. + * @return itself for chaining + */ + public MemoryInfo init(Runtime runtime){ + total = runtime.maxMemory(); + used = runtime.totalMemory() - runtime.freeMemory(); + return this; + } + + public long getTotal(){ + return total; + } + + public String getTotalFormated(){ + return formatMemory(getTotal()); + } + + public long getFree(){ + return getTotal() - getUsed(); + } + + public String getFreeFormated(){ + return formatMemory(getFree()); + } + + public long getUsed(){ + return used; + } + + public String getUsedFormated(){ + return formatMemory(getUsed()); + } + + public long getFreePercentage(){ + return getFree() * 100 / getTotal(); + } + + private String formatMemory(long bytes){ + if(bytes > 1024L*1024L){ + return bytes/(1024L *1024L) + " MB"; + } else if(bytes > 1024L){ + return bytes/(1024L) + " kB"; + } else { + return bytes + " B"; + } + } + + } + + public static class SystemInfo implements Serializable { + + protected String javaVersion; + protected String javaVendor; + protected String javaVm; + protected String javaVmVersion; + protected String javaRuntime; + protected String javaHome; + protected String osName; + protected String osArchitecture; + protected String osVersion; + protected String fileEncoding; + protected String userName; + protected String userDir; + protected String userTimezone; + protected String userLocale; + + public SystemInfo() { + } + + /** + * Fill object with info about current system loaded from {@link System} properties. + * @return object itself for chaining + */ + protected SystemInfo init(){ + javaVersion = System.getProperty("java.version"); + javaVendor = System.getProperty("java.vendor"); + javaVm = System.getProperty("java.vm.name"); + javaVmVersion = System.getProperty("java.vm.version"); + javaRuntime = System.getProperty("java.runtime.name"); + javaHome = System.getProperty("java.home"); + osName = System.getProperty("os.name"); + osArchitecture = System.getProperty("os.arch"); + osVersion = System.getProperty("os.version"); + fileEncoding = System.getProperty("file.encoding"); + userName = System.getProperty("user.name"); + userDir = System.getProperty("user.dir"); + userTimezone = System.getProperty("user.timezone"); + userLocale = (new Locale(System.getProperty("user.country"),System.getProperty("user.language")).toString()); + return this; + } + + public String getJavaVersion(){ + return javaVersion; + } + + public String getJavaVendor(){ + return javaVendor; + } + + public String getJavaVm(){ + return javaVm; + } + + public String getJavaVmVersion(){ + return javaVmVersion; + } + + public String getJavaRuntime(){ + return javaRuntime; + } + + public String getJavaHome(){ + return javaHome; + } + + public String getOsName(){ + return osName; + } + + public String getOsArchitecture(){ + return osArchitecture; + } + + public String getOsVersion(){ + return osVersion; + } + + public String getFileEncoding(){ + return fileEncoding; + } + + public String getUserName(){ + return userName; + } + + public String getUserDir(){ + return userDir; + } + + public String getUserTimezone(){ + return userTimezone; + } + + public String getUserLocale(){ + return userLocale; + } + } + + public static class ServerInfoRepresentation implements Serializable { + + private String version; + private String serverTime; + private long serverStartupTime; + + + private List providers; + + private MemoryInfo memoryInfo; + private SystemInfo systemInfo; + + public ServerInfoRepresentation() { + } + + public SystemInfo getSystemInfo(){ + return systemInfo; + } + + public MemoryInfo getMemoryInfo(){ + return memoryInfo; + } + + public String getServerTime() { + return serverTime; + } + + public long getServerStartupTime() { + return serverStartupTime; + } + + /** + * @return server startup time formatted + */ + public String getServerStartupTimeFormatted() { + return (new Date(serverStartupTime)).toString(); + } + + /** + * @return server uptime in millis + */ + public long getServerUptimeMillis(){ + return System.currentTimeMillis() - serverStartupTime; + } + + /** + * @return server uptime formatted like "0 days, 10 hours, 24 minutes, 55 seconds" + */ + public String getServerUptime(){ + long diffInSeconds = getServerUptimeMillis()/1000; + long diff[] = new long[] { 0, 0, 0, 0 }; + /* sec */diff[3] = (diffInSeconds >= 60 ? diffInSeconds % 60 : diffInSeconds); + /* min */diff[2] = (diffInSeconds = (diffInSeconds / 60)) >= 60 ? diffInSeconds % 60 : diffInSeconds; + /* hours */diff[1] = (diffInSeconds = (diffInSeconds / 60)) >= 24 ? diffInSeconds % 24 : diffInSeconds; + /* days */diff[0] = (diffInSeconds = (diffInSeconds / 24)); + + return String.format( + "%d day%s, %d hour%s, %d minute%s, %d second%s", + diff[0], + diff[0] != 1 ? "s" : "", + diff[1], + diff[1] != 1 ? "s" : "", + diff[2], + diff[2] != 1 ? "s" : "", + diff[3], + diff[3] != 1 ? "s" : ""); + } + + public String getVersion() { + return version; + } + + + public List getProviders() { + return providers; + } + } + + public static class SpiInfoRepresentation implements Serializable { + private String name; + private boolean internal; + private boolean systemInfo; + private Set implementations; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isInternal() { + return internal; + } + + public void setInternal(boolean internal) { + this.internal = internal; + } + + public Set getImplementations() { + return implementations; + } + + public boolean isSystemInfo() { + return systemInfo; + } + + public void setSystemInfo(boolean systemInfo) { + this.systemInfo = systemInfo; + } + + public void setImplementations(Set implementations) { + this.implementations = implementations; + } + } + + public static class SpiImplementationRepresentation implements Serializable { + + private String name; + private Map operationalInfo; + + public SpiImplementationRepresentation(String name) { + super(); + this.name = name; + } + + public Map getOperationalInfo() { + return operationalInfo; + } + + public void setOperationalInfo(Map operationalInfo) { + this.operationalInfo = operationalInfo; + } + + public String getName() { + return name; + } + + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index 2cca19a0c98..33ead317e41 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -317,13 +317,41 @@ public class AdminAPITest { Assert.assertNotNull(response); Assert.assertEquals(Version.VERSION, response.get("version")); Assert.assertNotNull(response.get("serverTime")); + Assert.assertNotNull(response.get("providers")); + Assert.assertNotNull(response.get("themes")); + Assert.assertNotNull(response.get("enums")); + + // System.out.println(response); + + } + + @Test + public void testServerInfoPage() { + + String token = createToken(); + final String authHeader = "Bearer " + token; + ClientRequestFilter authFilter = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); + } + }; + Client client = ClientBuilder.newBuilder().register(authFilter).build(); + UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth"); + WebTarget target = client.target(AdminRoot.adminBaseUrl(authBase).path("serverinfopage")); + + Map response = target.request().accept("application/json").get(Map.class); + + Assert.assertNotNull(response); + Assert.assertEquals(Version.VERSION, response.get("version")); + Assert.assertNotNull(response.get("serverTime")); + Assert.assertNotNull(response.get("providers")); Assert.assertNotNull(response.get("serverStartupTime")); Assert.assertNotNull(response.get("memoryInfo")); - Assert.assertNotNull(response.get("jpaInfo")); - Assert.assertNull(response.get("mongoDbInfo")); + Assert.assertNotNull(response.get("systemInfo")); - System.out.println(response); + // System.out.println(response); } From 244d3452d0a60acfb7c4cc2cca6187729a4bd62e Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Fri, 24 Jul 2015 11:07:01 +0200 Subject: [PATCH 13/13] KEYCLOAK-1542 - removed unused code from the page template --- .../base/admin/resources/partials/server-info.html | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 0dfac4cb9ac..991f2084010 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -96,19 +96,6 @@
    - -
    - Database Info -
    - - - - - -
    {{key}}{{value}}
    -
    -
    -
    Providers