diff --git a/CHANGELOG.md b/CHANGELOG.md index 03c48795..2af49afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ This replaces the static map `BaseService.clusterConfigs`. * Misc. improvements to logging and performance of `Cache` and `Timer`. +### 🎁 New Features +* New configuration property `hoist.sensitiveParamTerms` allows customization of environment +variables to be obscured in the admin client. + ## 22.0.0 - 2024-09-18 ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW) diff --git a/grails-app/domain/io/xh/hoist/config/AppConfig.groovy b/grails-app/domain/io/xh/hoist/config/AppConfig.groovy index 42d66788..480d6d98 100644 --- a/grails-app/domain/io/xh/hoist/config/AppConfig.groovy +++ b/grails-app/domain/io/xh/hoist/config/AppConfig.groovy @@ -17,7 +17,6 @@ import org.jasypt.util.text.BasicTextEncryptor import org.jasypt.util.text.TextEncryptor import static grails.async.Promises.task -import static io.xh.hoist.util.Utils.isSensitiveParamName class AppConfig implements JSONFormat, LogSupport { @@ -121,9 +120,6 @@ class AppConfig implements JSONFormat, LogSupport { // so we don't assume it can be parsed into the required type. Log failures on trace for // minimal visibility, but otherwise act as if the override value is not set. try { - if (isSensitiveParamName(name) && valueType != 'pwd') { - throw new RuntimeException("Refusing to return potentially sensitive override value from plain-text config - must be of type 'pwd'") - } return parseValue(overrideValue, opts) } catch (Throwable e) { logTrace("InstanceConfig override found for '$name' but cannot be parsed - override will be ignored", e.message) diff --git a/grails-app/services/io/xh/hoist/websocket/WebSocketService.groovy b/grails-app/services/io/xh/hoist/websocket/WebSocketService.groovy index 84179086..50065139 100644 --- a/grails-app/services/io/xh/hoist/websocket/WebSocketService.groovy +++ b/grails-app/services/io/xh/hoist/websocket/WebSocketService.groovy @@ -7,8 +7,6 @@ package io.xh.hoist.websocket -import grails.async.Promises -import grails.core.GrailsApplication import grails.events.EventPublisher import groovy.transform.CompileStatic import io.xh.hoist.BaseService @@ -23,6 +21,7 @@ import java.util.concurrent.ConcurrentHashMap import static grails.async.Promises.task import static grails.async.Promises.waitAll +import static io.xh.hoist.util.Utils.grailsConfig /** @@ -50,7 +49,6 @@ import static grails.async.Promises.waitAll @CompileStatic class WebSocketService extends BaseService implements EventPublisher { - GrailsApplication grailsApplication IdentityService identityService static final String HEARTBEAT_TOPIC = 'xhHeartbeat' @@ -62,7 +60,7 @@ class WebSocketService extends BaseService implements EventPublisher { private Map _channels = new ConcurrentHashMap<>() boolean isEnabled() { - return grailsApplication.config.getProperty('hoist.enableWebSockets', Boolean) + grailsConfig.getProperty('hoist.enableWebSockets', Boolean) } /** diff --git a/src/main/groovy/io/xh/hoist/configuration/ApplicationConfig.groovy b/src/main/groovy/io/xh/hoist/configuration/ApplicationConfig.groovy index a765550b..83e72669 100644 --- a/src/main/groovy/io/xh/hoist/configuration/ApplicationConfig.groovy +++ b/src/main/groovy/io/xh/hoist/configuration/ApplicationConfig.groovy @@ -20,9 +20,10 @@ class ApplicationConfig { static void defaultConfig(Script script) { withDelegate(script) { - // Read by WebSocketService to determine if WS support should generally be enabled. hoist { + // Read by WebSocketService to determine if WS support should generally be enabled. enableWebSockets = true + sensitiveParamTerms = ['password', 'passwrd', 'pwd', 'secret', 'tkn', 'token'] } spring { diff --git a/src/main/groovy/io/xh/hoist/util/Utils.groovy b/src/main/groovy/io/xh/hoist/util/Utils.groovy index 4b1c498b..fa1663cf 100644 --- a/src/main/groovy/io/xh/hoist/util/Utils.groovy +++ b/src/main/groovy/io/xh/hoist/util/Utils.groovy @@ -11,6 +11,7 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.JavaSerializer +import grails.config.Config import grails.util.Environment import grails.util.Holders import grails.util.Metadata @@ -151,10 +152,16 @@ class Utils { //------------------ // Other Singletons //------------------ + /** Get the grails application context. */ static ApplicationContext getAppContext() { return Holders.applicationContext } + /** Get the grails application config. */ + static Config getGrailsConfig() { + Holders.grailsApplication.config + } + /** Primary JDBC datasource, default backing DB for app Domain objects. */ static DataSource getDataSource() { return (DataSource) appContext.dataSource @@ -162,10 +169,7 @@ class Utils { /** Primary dataSource configuration. */ static Map getDataSourceConfig() { - Holders.grailsApplication - .config - .getProperty('dataSource', Map.class) - .collectEntries { it } + grailsConfig.getProperty('dataSource', Map.class).collectEntries { it } } static ExceptionHandler getExceptionHandler() { @@ -181,13 +185,14 @@ class Utils { } /** - * True if the given parameter name is likely sensitive and should not be serialized if not - * required. Used for built-in admin functionality accessed by trusted users only. - * NOT intended to be comprehensive or the last word on security! + * True if the given parameter name is likely sensitive and should not be serialized. + * To customize, set `hoist.sensitiveParamTerms` within your app's `application.groovy` to a + * list of terms that should trigger this behavior. + * + * See {@link io.xh.hoist.configuration.ApplicationConfig} for the default list. */ static boolean isSensitiveParamName(String name) { - def pattern = ~/(?i)(password|pwd|secret)$/ - pattern.matcher(name).find() + sensitiveParams.any { name.containsIgnoreCase(it) } } /** String parsing for a boolean. */ @@ -220,6 +225,7 @@ class Utils { c.call() } + static testSerialization(Object obj, Class clazz, LogSupport logSupport, Map opts) { Kryo kryo = new Kryo() kryo.registrationRequired = false @@ -251,4 +257,13 @@ class Utils { "${endTime - startTime}ms" ) } + + private static terms = null + private static List getSensitiveParams() { + if (terms == null) { + terms = grailsConfig.getProperty('hoist.sensitiveParamTerms', ArrayList.class) as List + } + return terms + } + }