KEYCLOAK-1542 - Server Info page extended by info about DB and MongoDB.

Functional test for /serverinfo REST endpoint added.
This commit is contained in:
Vlastimil Elias 2015-07-21 16:09:47 +02:00
parent dfb871c26a
commit 652b2fee86
10 changed files with 720 additions and 388 deletions

View file

@ -1,189 +1,242 @@
package org.keycloak.connections.jpa; 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.hibernate.ejb.AvailableSettings;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; 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.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory { 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 private DatabaseInfo databaseInfo;
public JpaConnectionProvider create(KeycloakSession session) {
lazyInit(session);
EntityManager em = emf.createEntityManager(); @Override
em = PersistenceExceptionConverter.create(em); public JpaConnectionProvider create(KeycloakSession session) {
session.getTransaction().enlist(new JpaKeycloakTransaction(em)); lazyInit(session);
return new DefaultJpaConnectionProvider(em);
}
@Override EntityManager em = emf.createEntityManager();
public void close() { em = PersistenceExceptionConverter.create(em);
if (emf != null) { session.getTransaction().enlist(new JpaKeycloakTransaction(em));
emf.close(); return new DefaultJpaConnectionProvider(em);
} }
}
@Override @Override
public String getId() { public void close() {
return "default"; if (emf != null) {
} emf.close();
}
}
@Override @Override
public void init(Config.Scope config) { public String getId() {
this.config = config; return "default";
} }
@Override @Override
public void postInit(KeycloakSessionFactory factory) { 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<String, Object> properties = new HashMap<String, Object>(); String databaseSchema = config.get("databaseSchema");
String unitName = "keycloak-default"; Map<String, Object> properties = new HashMap<String, Object>();
String dataSource = config.get("dataSource"); String unitName = "keycloak-default";
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"); String dataSource = config.get("dataSource");
if (user != null) { if (dataSource != null) {
properties.put(AvailableSettings.JDBC_USER, user); if (config.getBoolean("jta", false)) {
} properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
String password = config.get("password"); } else {
if (password != null) { properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
properties.put(AvailableSettings.JDBC_PASSWORD, password); }
} } else {
} properties.put(AvailableSettings.JDBC_URL, config.get("url"));
properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
String driverDialect = config.get("driverDialect"); String user = config.get("user");
if (driverDialect != null && driverDialect.length() > 0) { if (user != null) {
properties.put("hibernate.dialect", driverDialect); 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"); String driverDialect = config.get("driverDialect");
if (schema != null) { if (driverDialect != null && driverDialect.length() > 0) {
properties.put("hibernate.default_schema", schema); properties.put("hibernate.dialect", driverDialect);
} }
if (databaseSchema != null) { String schema = config.get("schema");
if (databaseSchema.equals("development-update")) { if (schema != null) {
properties.put("hibernate.hbm2ddl.auto", "update"); properties.put("hibernate.default_schema", schema);
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)); if (databaseSchema != null) {
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); 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) { properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
logger.trace("Updating database"); properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); connection = getConnection();
if (updater == null) { prepareDatabaseInfo(connection);
throw new RuntimeException("Can't update database: JPA updater provider not found");
}
connection = getConnection(); if (databaseSchema != null) {
logger.trace("Updating database");
if (databaseSchema.equals("update")) { JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
String currentVersion = null; if (updater == null) {
try { throw new RuntimeException("Can't update database: JPA updater provider not found");
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)) { if (databaseSchema.equals("update")) {
updater.update(session, connection, schema); String currentVersion = null;
} else { try {
logger.debug("Database is up to date"); ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql(schema));
} if (resultSet.next()) {
} else if (databaseSchema.equals("validate")) { currentVersion = resultSet.getString(1);
updater.validate(connection, schema); }
} else { } catch (SQLException e) {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); }
}
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"); logger.trace("Database update completed");
emf = Persistence.createEntityManagerFactory(unitName, properties); }
logger.trace("EntityManagerFactory created");
// Close after creating EntityManagerFactory to prevent in-mem databases from closing logger.trace("Creating EntityManagerFactory");
if (connection != null) { emf = Persistence.createEntityManagerFactory(unitName, properties);
try { logger.trace("EntityManagerFactory created");
connection.close();
} catch (SQLException e) {
logger.warn(e);
}
}
}
}
}
}
private Connection getConnection() { // Close after creating EntityManagerFactory to prevent in-mem databases from closing
try { if (connection != null) {
String dataSourceLookup = config.get("dataSource"); try {
if (dataSourceLookup != null) { connection.close();
DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); } catch (SQLException e) {
return dataSource.getConnection(); logger.warn(e);
} 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); }
}
} 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;
}
}
} }

View file

@ -1,9 +1,10 @@
package org.keycloak.connections.jpa; package org.keycloak.connections.jpa;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.MonitorableProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public interface JpaConnectionProviderFactory extends ProviderFactory<JpaConnectionProvider> { public interface JpaConnectionProviderFactory extends MonitorableProviderFactory<JpaConnectionProvider> {
} }

View file

@ -1,10 +1,11 @@
package org.keycloak.connections.mongo; package org.keycloak.connections.mongo;
import com.mongodb.DB; import java.lang.reflect.Method;
import com.mongodb.MongoClient; import java.net.UnknownHostException;
import com.mongodb.MongoClientOptions; import java.util.Collections;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress; import javax.net.ssl.SSLSocketFactory;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.connections.mongo.api.MongoStore; 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.connections.mongo.updater.MongoUpdaterProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderOperationalInfo;
import javax.net.ssl.SSLSocketFactory; import com.mongodb.DB;
import java.lang.reflect.Method; import com.mongodb.MongoClient;
import java.net.UnknownHostException; import com.mongodb.MongoClientOptions;
import java.util.Collections; import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory {
// TODO Make configurable // TODO Make configurable
private String[] entities = new String[]{ 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.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.CredentialEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.entities.FederatedIdentityEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.sessions.mongo.entities.MongoUsernameLoginFailureEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", "org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity", "org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity", "org.keycloak.models.entities.UserFederationProviderEntity",
"org.keycloak.models.entities.IdentityProviderEntity", "org.keycloak.models.entities.UserFederationMapperEntity", "org.keycloak.models.entities.ProtocolMapperEntity", "org.keycloak.models.entities.IdentityProviderMapperEntity",
"org.keycloak.models.entities.ClientIdentityProviderMappingEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.entities.AuthenticationExecutionEntity",
"org.keycloak.models.entities.RequiredCredentialEntity", "org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.RequiredActionProviderEntity", };
"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 MongoStore mongoStore;
private DB db; private DB db;
protected Config.Scope config; protected Config.Scope config;
@Override private MongoDbInfo mongoDbInfo;
public MongoConnectionProvider create(KeycloakSession session) {
lazyInit(session);
TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); @Override
session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext)); public MongoConnectionProvider create(KeycloakSession session) {
return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext); lazyInit(session);
}
@Override TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore);
public void init(Config.Scope config) { session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext));
this.config = config; return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext);
} }
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public void init(Config.Scope config) {
this.config = config;
}
} @Override
public void postInit(KeycloakSessionFactory factory) {
}
private void lazyInit(KeycloakSession session) { private void lazyInit(KeycloakSession session) {
if (client == null) { if (client == null) {
synchronized (this) { synchronized (this) {
if (client == null) { if (client == null) {
try { try {
this.client = createMongoClient(); this.client = createMongoClient();
String dbName = config.get("db", "keycloak"); String dbName = config.get("db", "keycloak");
this.db = client.getDB(dbName); this.db = client.getDB(dbName);
String databaseSchema = config.get("databaseSchema"); String databaseSchema = config.get("databaseSchema");
if (databaseSchema != null) { if (databaseSchema != null) {
if (databaseSchema.equals("update")) { if (databaseSchema.equals("update")) {
MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class); MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class);
if (mongoUpdater == null) { if (mongoUpdater == null) {
throw new RuntimeException("Can't update database: Mongo updater provider not found"); throw new RuntimeException("Can't update database: Mongo updater provider not found");
} }
mongoUpdater.update(session, db); mongoUpdater.update(session, db);
} else { } else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema); throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
} }
} }
this.mongoStore = new MongoStoreImpl(db, getManagedEntities()); this.mongoStore = new MongoStoreImpl(db, getManagedEntities());
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }
} }
} }
private Class[] getManagedEntities() throws ClassNotFoundException { private Class[] getManagedEntities() throws ClassNotFoundException {
Class[] entityClasses = new Class[entities.length]; Class[] entityClasses = new Class[entities.length];
for (int i = 0; i < entities.length; i++) { for (int i = 0; i < entities.length; i++) {
entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]); entityClasses[i] = Thread.currentThread().getContextClassLoader().loadClass(entities[i]);
} }
return entityClasses; return entityClasses;
} }
@Override @Override
public void close() { public void close() {
if (client != null) { if (client != null) {
client.close(); client.close();
} }
} }
@Override @Override
public String getId() { public String getId() {
return "default"; return "default";
} }
/** /**
* Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client * Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo
* from different source. * client from different source.
* *
* This method can assume that "config" is already set and can use it. * This method can assume that "config" is already set and can use it.
* *
* @return mongoClient instance, which will be shared for whole Keycloak * @return mongoClient instance, which will be shared for whole Keycloak
* *
* @throws UnknownHostException * @throws UnknownHostException
*/ */
protected MongoClient createMongoClient() throws UnknownHostException { protected MongoClient createMongoClient() throws UnknownHostException {
String host = config.get("host", ServerAddress.defaultHost()); String host = config.get("host", ServerAddress.defaultHost());
int port = config.getInt("port", ServerAddress.defaultPort()); int port = config.getInt("port", ServerAddress.defaultPort());
String dbName = config.get("db", "keycloak"); String dbName = config.get("db", "keycloak");
String user = config.get("user"); String user = config.get("user");
String password = config.get("password"); String password = config.get("password");
MongoClientOptions clientOptions = getClientOptions(); MongoClientOptions clientOptions = getClientOptions();
MongoClient client; MongoClient client;
if (user != null && password != null) { if (user != null && password != null) {
MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray()); MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray());
client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions); client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions);
} else { } else {
client = new MongoClient(new ServerAddress(host, port), clientOptions); client = new MongoClient(new ServerAddress(host, port), clientOptions);
} }
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); mongoDbInfo = new MongoDbInfo();
return client; mongoDbInfo.driverVersion = client.getVersion();
} mongoDbInfo.address = client.getAddress().toString();
mongoDbInfo.database = dbName;
mongoDbInfo.user = user;
protected MongoClientOptions getClientOptions() { logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
MongoClientOptions.Builder builder = MongoClientOptions.builder(); return client;
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());
}
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) { return builder.build();
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) { protected void checkBooleanOption(String optionName, MongoClientOptions.Builder builder) {
Integer val = config.getInt(optionName); Boolean val = config.getBoolean(optionName);
if (val != null) { if (val != null) {
try { try {
Method m = MongoClientOptions.Builder.class.getMethod(optionName, int.class); Method m = MongoClientOptions.Builder.class.getMethod(optionName, boolean.class);
m.invoke(builder, val); m.invoke(builder, val);
} catch (Exception e) { } 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); 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;
}
}
} }

View file

@ -1,9 +1,9 @@
package org.keycloak.connections.mongo; package org.keycloak.connections.mongo;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.MonitorableProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public interface MongoConnectionProviderFactory extends ProviderFactory<MongoConnectionProvider> { public interface MongoConnectionProviderFactory extends MonitorableProviderFactory<MongoConnectionProvider> {
} }

View file

@ -1,21 +1,47 @@
<div class="col-md-12"> <div class="col-md-12">
<h1>Server Info</h1> <h1>Server Info</h1>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<tr>
<td width="20%">Keycloak Version</td>
<td>{{serverInfo.version}}</td>
</tr>
<tr>
<td>Server Time</td>
<td>{{serverInfo.serverTime}} (<a style="cursor: pointer" data-ng-click="serverInfoUpdate()">update</a>)</td>
</tr>
<tr>
<td>Server Uptime</td>
<td>{{serverInfo.serverUptime}}</td>
</tr>
</table>
<fieldset>
<legend>Java VM Memory Statistics</legend>
<div class="form-group">
<table class="table table-striped table-bordered" style="margin-top: 0px;">
<tr>
<td width="20%">Total Memory</td>
<td>{{serverInfo.memoryInfo.totalFormated}}</td>
</tr>
<tr>
<td>Free Memory</td>
<td>{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)</td>
</tr>
<tr>
<td>Used Memory</td>
<td>{{serverInfo.memoryInfo.usedFormated}}</td>
</tr>
</table>
</div>
</fieldset>
<fieldset>
<legend collapsed>System Info</legend>
<div class="form-group">
<table class="table table-striped table-bordered" style="margin-top: 0px;">
<tr> <tr>
<td>Version</td> <td width="20%">Current Working Directory</td>
<td>{{serverInfo.version}}</td>
</tr>
<tr>
<td>Server Time</td>
<td>{{serverInfo.serverTime}} (<a style="cursor: pointer" data-ng-click="serverInfoUpdate()">update</a>)</td>
</tr>
<tr>
<td>Server Uptime</td>
<td>{{serverInfo.serverUptime}}</td>
</tr>
<tr>
<td>Current Working Directory</td>
<td>{{serverInfo.systemInfo.userDir}}</td> <td>{{serverInfo.systemInfo.userDir}}</td>
</tr> </tr>
<tr> <tr>
@ -66,23 +92,59 @@
<td>OS Architecture</td> <td>OS Architecture</td>
<td>{{serverInfo.systemInfo.osArchitecture}}</td> <td>{{serverInfo.systemInfo.osArchitecture}}</td>
</tr> </tr>
</table> </table>
</div>
</fieldset>
<fieldset ng-show="serverInfo.jpaInfo">
<legend collapsed>Database Info</legend>
<div class="form-group">
<table class="table table-striped table-bordered" style="margin-top: 0px;">
<tr>
<td width="20%">Database URL</td>
<td>{{serverInfo.jpaInfo.jdbcUrl}}</td>
</tr>
<tr>
<td>Database User</td>
<td>{{serverInfo.jpaInfo.databaseUser}}</td>
</tr>
<tr>
<td>Database Type</td>
<td>{{serverInfo.jpaInfo.databaseProduct}}</td>
</tr>
<tr>
<td>Database Driver</td>
<td>{{serverInfo.jpaInfo.databaseDriver}}</td>
</tr>
</table>
</div>
</fieldset>
<fieldset ng-show="serverInfo.mongoDbInfo">
<legend collapsed>Mongo DB Info</legend>
<div class="form-group">
<table class="table table-striped table-bordered" style="margin-top: 0px;">
<tr width="20%">
<td>Address</td>
<td>{{serverInfo.mongoDbInfo.address}}</td>
</tr>
<tr>
<td>Database</td>
<td>{{serverInfo.mongoDbInfo.database}}</td>
</tr>
<tr>
<td>User</td>
<td>{{serverInfo.mongoDbInfo.user}}</td>
</tr>
<tr>
<td>Driver Version</td>
<td>{{serverInfo.mongoDbInfo.driverVersion}}</td>
</tr>
</table>
</div>
</fieldset>
<h3>Java VM Memory Statistics</h3>
<table class="table table-striped table-bordered">
<tr>
<td>Total Memory</td>
<td>{{serverInfo.memoryInfo.totalFormated}}</td>
</tr>
<tr>
<td>Free Memory</td>
<td>{{serverInfo.memoryInfo.freeFormated}} ({{serverInfo.memoryInfo.freePercentage}}%)</td>
</tr>
<tr>
<td>Used Memory</td>
<td>{{serverInfo.memoryInfo.usedFormated}}</td>
</tr>
</table>
<fieldset> <fieldset>
<legend collapsed>Providers</legend> <legend collapsed>Providers</legend>
@ -93,7 +155,7 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>SPI</th> <th width="20%">SPI</th>
<th>Providers</th> <th>Providers</th>
</tr> </tr>
</thead> </thead>
@ -117,7 +179,7 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>SPI</th> <th width="20%">SPI</th>
<th>Providers</th> <th>Providers</th>
</tr> </tr>
</thead> </thead>

View file

@ -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<T extends Provider> extends ProviderFactory<T> {
/**
* 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();
}

View file

@ -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();
}

View file

@ -39,6 +39,21 @@
<artifactId>keycloak-connections-http-client</artifactId> <artifactId>keycloak-connections-http-client</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-jpa</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-mongo</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-forms-common-freemarker</artifactId> <artifactId>keycloak-forms-common-freemarker</artifactId>

View file

@ -1,5 +1,6 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -13,9 +14,13 @@ import java.util.Set;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import org.jboss.logging.Logger;
import org.keycloak.Version; import org.keycloak.Version;
import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory; 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.EventListenerProvider;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType; 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.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory; import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.provider.MonitorableProviderFactory;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderOperationalInfo;
import org.keycloak.provider.Spi; import org.keycloak.provider.Spi;
import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@ -42,6 +49,8 @@ import org.keycloak.social.SocialIdentityProvider;
*/ */
public class ServerInfoAdminResource { public class ServerInfoAdminResource {
private static final Logger logger = Logger.getLogger(ServerInfoAdminResource.class);
private static final Map<String, List<String>> ENUMS = createEnumsMap(EventType.class, OperationType.class); private static final Map<String, List<String>> ENUMS = createEnumsMap(EventType.class, OperationType.class);
@Context @Context
@ -58,6 +67,8 @@ public class ServerInfoAdminResource {
info.version = Version.VERSION; info.version = Version.VERSION;
info.serverTime = new Date().toString(); info.serverTime = new Date().toString();
info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp(); info.serverStartupTime = session.getKeycloakSessionFactory().getServerStartupTimestamp();
info.memoryInfo = (new MemoryInfo()).init(Runtime.getRuntime());
info.systemInfo = (new SystemInfo()).init();
setSocialProviders(info); setSocialProviders(info);
setIdentityProviders(info); setIdentityProviders(info);
setThemes(info); setThemes(info);
@ -68,6 +79,21 @@ public class ServerInfoAdminResource {
setProtocolMapperTypes(info); setProtocolMapperTypes(info);
setBuiltinProtocolMappers(info); setBuiltinProtocolMappers(info);
info.setEnums(ENUMS); info.setEnums(ENUMS);
ProviderFactory<JpaConnectionProvider> 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<MongoConnectionProvider> 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; 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(){ public long getTotal(){
return Runtime.getRuntime().maxMemory(); return total;
} }
public String getTotalFormated(){ public String getTotalFormated(){
@ -210,7 +254,7 @@ public class ServerInfoAdminResource {
} }
public long getUsed(){ public long getUsed(){
return Runtime.getRuntime().totalMemory(); return used;
} }
public String getUsedFormated(){ public String getUsedFormated(){
@ -218,7 +262,7 @@ public class ServerInfoAdminResource {
} }
public long getFreePercentage(){ public long getFreePercentage(){
return getFree()*100/getTotal(); return getFree() * 100 / getTotal();
} }
private String formatMemory(long bytes){ 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(){ public String getJavaVersion(){
return System.getProperty("java.version"); return javaVersion;
} }
public String getJavaVendor(){ public String getJavaVendor(){
return System.getProperty("java.vendor"); return javaVendor;
} }
public String getJavaVm(){ public String getJavaVm(){
return System.getProperty("java.vm.name"); return javaVm;
} }
public String getJavaVmVersion(){ public String getJavaVmVersion(){
return System.getProperty("java.vm.version"); return javaVmVersion;
} }
public String getJavaRuntime(){ public String getJavaRuntime(){
return System.getProperty("java.runtime.name"); return javaRuntime;
} }
public String getJavaHome(){ public String getJavaHome(){
return System.getProperty("java.home"); return javaHome;
} }
public String getOsName(){ public String getOsName(){
return System.getProperty("os.name"); return osName;
} }
public String getOsArchitecture(){ public String getOsArchitecture(){
return System.getProperty("os.arch"); return osArchitecture;
} }
public String getOsVersion(){ public String getOsVersion(){
return System.getProperty("os.version"); return osVersion;
} }
public String getFileEncoding(){ public String getFileEncoding(){
return System.getProperty("file.encoding"); return fileEncoding;
} }
public String getUserName(){ public String getUserName(){
return System.getProperty("user.name"); return userName;
} }
public String getUserDir(){ public String getUserDir(){
return System.getProperty("user.dir"); return userDir;
} }
public String getUserTimezone(){ public String getUserTimezone(){
return System.getProperty("user.timezone"); return userTimezone;
} }
public String getUserLocale(){ 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 version;
private String serverTime; private String serverTime;
private long serverStartupTime; private long serverStartupTime;
private Map<String, List<String>> themes; private Map<String, List<String>> themes;
@ -315,25 +397,43 @@ public class ServerInfoAdminResource {
private Map<String, List<String>> enums; private Map<String, List<String>> enums;
private MemoryInfo memoryInfo;
private SystemInfo systemInfo;
private ProviderOperationalInfo jpaInfo;
private ProviderOperationalInfo mongoDbInfo;
public ServerInfoRepresentation() { public ServerInfoRepresentation() {
} }
public SystemInfo getSystemInfo(){ public SystemInfo getSystemInfo(){
return new SystemInfo(); return systemInfo;
} }
public MemoryInfo getMemoryInfo(){ public MemoryInfo getMemoryInfo(){
return new MemoryInfo(); return memoryInfo;
}
public ProviderOperationalInfo getJpaInfo() {
return jpaInfo;
}
public ProviderOperationalInfo getMongoDbInfo() {
return mongoDbInfo;
} }
public String getServerTime() { public String getServerTime() {
return serverTime; return serverTime;
} }
public long getServerStartupTime() {
return serverStartupTime;
}
/** /**
* @return server startup time formatted * @return server startup time formatted
*/ */
public String getServerStartupTime() { public String getServerStartupTimeFormatted() {
return (new Date(serverStartupTime)).toString(); 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 String name;
private boolean internal; private boolean internal;
private Set<String> implementations; private Set<String> implementations;

View file

@ -21,10 +21,27 @@
*/ */
package org.keycloak.testsuite.admin; 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.Assert;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.Version;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -40,22 +57,8 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.KeycloakServer; import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
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;
/** /**
* Tests Undertow Adapter * Tests Undertow Adapter
@ -295,4 +298,34 @@ public class AdminAPITest {
testCreateRealm("/admin-test/testrealm.json"); 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);
}
} }