/* * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. * * Copyright (c) 2014, Gluu */ package org.xdi.oxauth.model.config; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jettison.json.JSONObject; import org.gluu.site.ldap.persistence.LdapEntryManager; import org.gluu.site.ldap.persistence.exception.LdapMappingException; import org.slf4j.Logger; import org.xdi.exception.ConfigurationException; import org.xdi.oxauth.model.configuration.AppConfiguration; import org.xdi.oxauth.model.configuration.Configuration; import org.xdi.oxauth.model.crypto.AbstractCryptoProvider; import org.xdi.oxauth.model.error.ErrorMessages; import org.xdi.oxauth.model.error.ErrorResponseFactory; import org.xdi.oxauth.service.AppInitializer; import org.xdi.oxauth.util.ServerUtil; import org.xdi.service.cdi.event.ConfigurationEvent; import org.xdi.service.cdi.event.ConfigurationUpdate; import org.xdi.service.cdi.event.LdapConfigurationReload; import org.xdi.service.cdi.event.Scheduled; import org.xdi.service.timer.event.TimerEvent; import org.xdi.service.timer.schedule.TimerSchedule; import org.xdi.util.StringHelper; import org.xdi.util.properties.FileConfiguration; import javax.annotation.PostConstruct; import javax.ejb.Asynchronous; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.enterprise.inject.Instance; import javax.enterprise.inject.Produces; import javax.inject.Inject; import javax.inject.Named; import java.io.File; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Yuriy Zabrovarnyy * @author Javier Rojas Blum * @author Yuriy Movchan * @version June 15, 2016 */ @ApplicationScoped @Named public class ConfigurationFactory { @Inject private Logger log; @Inject private Event<TimerEvent> timerEvent; @Inject private Event<AppConfiguration> configurationUpdateEvent; @Inject private Event<String> event; @Inject @Named(AppInitializer.LDAP_ENTRY_MANAGER_NAME) private Instance<LdapEntryManager> ldapEntryManagerInstance; @Inject private Instance<Configuration> configurationInstance; public final static String LDAP_CONFIGUARION_RELOAD_EVENT_TYPE = "ldapConfigurationReloadEvent"; private final static int DEFAULT_INTERVAL = 30; // 30 seconds static { if (System.getProperty("gluu.base") != null) { BASE_DIR = System.getProperty("gluu.base"); } else if ((System.getProperty("catalina.base") != null) && (System.getProperty("catalina.base.ignore") == null)) { BASE_DIR = System.getProperty("catalina.base"); } else if (System.getProperty("catalina.home") != null) { BASE_DIR = System.getProperty("catalina.home"); } else if (System.getProperty("jboss.home.dir") != null) { BASE_DIR = System.getProperty("jboss.home.dir"); } else { BASE_DIR = null; } } private static final String BASE_DIR; private static final String DIR = BASE_DIR + File.separator + "conf" + File.separator; private static final String LDAP_FILE_PATH = DIR + "oxauth-ldap.properties"; public static final String LDAP_DEFAULT_FILE_PATH = DIR + "ox-ldap.properties"; private final String CONFIG_FILE_NAME = "oxauth-config.json"; private final String ERRORS_FILE_NAME = "oxauth-errors.json"; private final String STATIC_CONF_FILE_NAME = "oxauth-static-conf.json"; private final String WEB_KEYS_FILE_NAME = "oxauth-web-keys.json"; private final String SALT_FILE_NAME = "salt"; private String confDir, configFilePath, errorsFilePath, staticConfFilePath, webKeysFilePath, saltFilePath; private boolean loaded = false; private FileConfiguration ldapConfiguration; private AppConfiguration conf; private StaticConfiguration staticConf; private WebKeysConfiguration jwks; private ErrorResponseFactory errorResponseFactory; private String cryptoConfigurationSalt; private AtomicBoolean isActive; private String prevLdapFileName; private long ldapFileLastModifiedTime = -1; private long loadedRevision = -1; private boolean loadedFromLdap = true; @PostConstruct public void init() { this.isActive = new AtomicBoolean(true); try { String ldapFileName = determineLdapConfigurationFileName(); this.prevLdapFileName = loadLdapConfiguration(ldapFileName); this.confDir = confDir(); this.configFilePath = confDir + CONFIG_FILE_NAME; this.errorsFilePath = confDir + ERRORS_FILE_NAME; this.staticConfFilePath = confDir + STATIC_CONF_FILE_NAME; String certsDir = getLdapConfiguration().getString("certsDir"); if (StringHelper.isEmpty(certsDir)) { certsDir = confDir; } this.webKeysFilePath = certsDir + File.separator + WEB_KEYS_FILE_NAME; this.saltFilePath = confDir + SALT_FILE_NAME; loadCryptoConfigurationSalt(); } finally { this.isActive.set(false); } } public void create() { if (!createFromLdap(true)) { log.error("Failed to load configuration from LDAP. Please fix it!!!."); throw new ConfigurationException("Failed to load configuration from LDAP."); } else { log.info("Configuration loaded successfully."); } } public void initTimer() { log.debug("Initializing Configuration Timer"); final int delay = 30; final int interval = DEFAULT_INTERVAL; timerEvent.fire(new TimerEvent(new TimerSchedule(delay, interval), new ConfigurationEvent(), Scheduled.Literal.INSTANCE)); } @Asynchronous public void reloadConfigurationTimerEvent(@Observes @Scheduled ConfigurationEvent configurationEvent) { if (this.isActive.get()) { return; } if (!this.isActive.compareAndSet(false, true)) { return; } try { reloadConfiguration(); } catch (Throwable ex) { log.error("Exception happened while reloading application configuration", ex); } finally { this.isActive.set(false); } } private void reloadConfiguration() { // Reload LDAP configuration if needed String ldapFileName = determineLdapConfigurationFileName(); File ldapFile = new File(ldapFileName); if (ldapFile.exists()) { final long lastModified = ldapFile.lastModified(); if (!StringHelper.equalsIgnoreCase(this.prevLdapFileName, ldapFileName) || (lastModified > ldapFileLastModifiedTime)) { // Reload configuration only if it was modified this.prevLdapFileName = loadLdapConfiguration(ldapFileName); event.select(LdapConfigurationReload.Literal.INSTANCE).fire(LDAP_CONFIGUARION_RELOAD_EVENT_TYPE); } } if (!loadedFromLdap) { return; } final Conf conf = loadConfigurationFromLdap("oxRevision"); if (conf == null) { return; } if (conf.getRevision() <= this.loadedRevision) { return; } createFromLdap(false); } private String confDir() { final String confDir = getLdapConfiguration().getString("confDir", null); if (StringUtils.isNotBlank(confDir)) { return confDir; } return DIR; } public FileConfiguration getLdapConfiguration() { return ldapConfiguration; } @Produces @ApplicationScoped public AppConfiguration getAppConfiguration() { return conf; } @Produces @ApplicationScoped public StaticConfiguration getStaticConfiguration() { return staticConf; } @Produces @ApplicationScoped public WebKeysConfiguration getWebKeysConfiguration() { return jwks; } @Produces @ApplicationScoped public ErrorResponseFactory getErrorResponseFactory() { return errorResponseFactory; } public BaseDnConfiguration getBaseDn() { return getStaticConfiguration().getBaseDn(); } public String getCryptoConfigurationSalt() { return cryptoConfigurationSalt; } private boolean createFromFile() { boolean result = reloadConfFromFile() && reloadErrorsFromFile() && reloadStaticConfFromFile() && reloadWebkeyFromFile(); return result; } private boolean reloadWebkeyFromFile() { final WebKeysConfiguration webKeysFromFile = loadWebKeysFromFile(); if (webKeysFromFile != null) { log.info("Reloaded web keys from file: " + webKeysFilePath); jwks = webKeysFromFile; return true; } else { log.error("Failed to load web keys configuration from file: " + webKeysFilePath); } return false; } private boolean reloadStaticConfFromFile() { final StaticConfiguration staticConfFromFile = loadStaticConfFromFile(); if (staticConfFromFile != null) { log.info("Reloaded static conf from file: " + staticConfFilePath); staticConf = staticConfFromFile; return true; } else { log.error("Failed to load static configuration from file: " + staticConfFilePath); } return false; } private boolean reloadErrorsFromFile() { final ErrorMessages errorsFromFile = loadErrorsFromFile(); if (errorsFromFile != null) { log.info("Reloaded errors from file: " + errorsFilePath); errorResponseFactory = new ErrorResponseFactory(errorsFromFile); return true; } else { log.error("Failed to load errors from file: " + errorsFilePath); } return false; } private boolean reloadConfFromFile() { final AppConfiguration configFromFile = loadConfFromFile(); if (configFromFile != null) { log.info("Reloaded configuration from file: " + configFilePath); conf = configFromFile; return true; } else { log.error("Failed to load configuration from file: " + configFilePath); } return false; } private boolean createFromLdap(boolean recoverFromFiles) { log.info("Loading configuration from LDAP..."); try { final Conf c = loadConfigurationFromLdap(); if (c != null) { init(c); // Destroy old configuration if (this.loaded) { destroy(AppConfiguration.class); destroy(StaticConfiguration.class); destroy(WebKeysConfiguration.class); destroy(ErrorResponseFactory.class); } this.loaded = true; configurationUpdateEvent.select(ConfigurationUpdate.Literal.INSTANCE).fire(conf); return true; } } catch (Exception ex) { log.error(ex.getMessage(), ex); } if (recoverFromFiles) { log.info("Unable to find configuration in LDAP, try to load configuration from file system... "); if (createFromFile()) { this.loadedFromLdap = false; return true; } } return false; } public void destroy(Class<? extends Configuration> clazz) { Instance<? extends Configuration> confInstance = configurationInstance.select(clazz); configurationInstance.destroy(confInstance.get()); } private Conf loadConfigurationFromLdap(String... returnAttributes) { final LdapEntryManager ldapManager = ldapEntryManagerInstance.get(); final String dn = getLdapConfiguration().getString("oxauth_ConfigurationEntryDN"); try { final Conf conf = ldapManager.find(Conf.class, dn, returnAttributes); return conf; } catch (LdapMappingException ex) { log.error(ex.getMessage()); } return null; } private void init(Conf p_conf) { initConfigurationFromJson(p_conf.getDynamic()); initStaticConfigurationFromJson(p_conf.getStatics()); initWebKeysFromJson(p_conf.getWebKeys()); initErrorsFromJson(p_conf.getErrors()); this.loadedRevision = p_conf.getRevision(); } private void initWebKeysFromJson(String p_webKeys) { try { initJwksFromString(p_webKeys); } catch (Exception ex) { log.error("Failed to load JWKS. Attempting to generate new JWKS...", ex); String newWebKeys = null; try { // Generate new JWKS JSONObject jsonObject = AbstractCryptoProvider.generateJwks( getAppConfiguration().getKeyRegenerationInterval(), getAppConfiguration().getIdTokenLifetime(), getAppConfiguration()); newWebKeys = jsonObject.toString(); // Attempt to load new JWKS initJwksFromString(newWebKeys); // Store new JWKS in LDAP Conf conf = loadConfigurationFromLdap(); conf.setWebKeys(newWebKeys); long nextRevision = conf.getRevision() + 1; conf.setRevision(nextRevision); final LdapEntryManager ldapManager = ldapEntryManagerInstance.get(); ldapManager.merge(conf); log.info("New JWKS generated successfully"); } catch (Exception ex2) { log.error("Failed to re-generate JWKS keys", ex2); } } } public void initJwksFromString(String p_webKeys) throws IOException, JsonParseException, JsonMappingException { final WebKeysConfiguration k = ServerUtil.createJsonMapper().readValue(p_webKeys, WebKeysConfiguration.class); if (k != null) { jwks = k; } } private void initStaticConfigurationFromJson(String p_statics) { try { final StaticConfiguration c = ServerUtil.createJsonMapper().readValue(p_statics, StaticConfiguration.class); if (c != null) { staticConf = c; } } catch (Exception e) { log.error(e.getMessage(), e); } } private void initConfigurationFromJson(String p_configurationJson) { try { final AppConfiguration c = ServerUtil.createJsonMapper().readValue(p_configurationJson, AppConfiguration.class); if (c != null) { conf = c; } } catch (Exception e) { log.error(e.getMessage(), e); } } private void initErrorsFromJson(String p_errosAsJson) { try { final ErrorMessages errorMessages = ServerUtil.createJsonMapper().readValue(p_errosAsJson, ErrorMessages.class); if (errorMessages != null) { errorResponseFactory = new ErrorResponseFactory(errorMessages); } } catch (Exception e) { log.error(e.getMessage(), e); } } private String loadLdapConfiguration(String ldapFileName) { try { ldapConfiguration = new FileConfiguration(ldapFileName); File ldapFile = new File(ldapFileName); if (ldapFile.exists()) { this.ldapFileLastModifiedTime = ldapFile.lastModified(); } return ldapFileName; } catch (Exception e) { log.error(e.getMessage(), e); ldapConfiguration = null; } return null; } private String determineLdapConfigurationFileName() { File ldapFile = new File(LDAP_FILE_PATH); if (ldapFile.exists()) { return LDAP_FILE_PATH; } return LDAP_DEFAULT_FILE_PATH; } private AppConfiguration loadConfFromFile() { try { return ServerUtil.createJsonMapper().readValue(new File(configFilePath), AppConfiguration.class); } catch (Exception e) { log.warn(e.getMessage(), e); } return null; } private ErrorMessages loadErrorsFromFile() { try { return ServerUtil.createJsonMapper().readValue(new File(errorsFilePath), ErrorMessages.class); } catch (Exception e) { log.warn(e.getMessage(), e); } return null; } private StaticConfiguration loadStaticConfFromFile() { try { return ServerUtil.createJsonMapper().readValue(new File(staticConfFilePath), StaticConfiguration.class); } catch (Exception e) { log.warn(e.getMessage(), e); } return null; } private WebKeysConfiguration loadWebKeysFromFile() { try { return ServerUtil.createJsonMapper().readValue(new File(webKeysFilePath), WebKeysConfiguration.class); } catch (Exception e) { log.warn(e.getMessage(), e); } return null; } public void loadCryptoConfigurationSalt() { try { FileConfiguration cryptoConfiguration = createFileConfiguration(saltFilePath, true); this.cryptoConfigurationSalt = cryptoConfiguration.getString("encodeSalt"); } catch (Exception ex) { log.error("Failed to load configuration from {}", ex, saltFilePath); throw new ConfigurationException("Failed to load configuration from " + saltFilePath, ex); } } private FileConfiguration createFileConfiguration(String fileName, boolean isMandatory) { try { FileConfiguration fileConfiguration = new FileConfiguration(fileName); return fileConfiguration; } catch (Exception ex) { if (isMandatory) { log.error("Failed to load configuration from {}", ex, fileName); throw new ConfigurationException("Failed to load configuration from " + fileName, ex); } } return null; } }