Skip to content

Commit b4b41b0

Browse files
committed
GROOVY-12270: Map connector authentication properties onto the keys a connector consumes
JmxBuilder's connectorServer documents properties:[authenticate:true, passwordFile:..., accessFile:...], and wrote them into the connector environment under com.sun.management.jmxremote.* names. Those names belong to the JDK's out-of-the-box management agent, not to a connector server. The agent reads them and translates them into the jmx.remote.x.* names the connector actually consumes, then installs the authenticator itself; see sun.management.jmxremote.ConnectorBootstrap. Nothing performed that translation here, so an operator following the documented syntax started a connector with no authenticator at all. Verified rather than reasoned: with the environment this class built, the connector reported no authenticator and a credential-less client connected and read the MBean count; with jmx.remote.x.password.file the same client is rejected with "Authentication failed! Credentials required". Translate the aliases, and only when authentication was requested. Add loginConfig for JAAS, mapping to jmx.remote.x.login.config. Reject authenticate:true with no source of credentials at all, since that asks for authentication and would otherwise start open, which is the failure being fixed; a caller-supplied jmx.remote.authenticator counts as such a source, since passing a JMXAuthenticator through is the standard JSR-160 route for custom authentication. The com.sun.management.jmxremote.* names remain accepted as input spellings but are no longer copied into the environment, where they mean nothing; the ssl alias is likewise consumed to select the socket factories rather than passed through. Three GROOVY-12119 tests asserted the presence of those inert keys as a witness that the environment map was not discarded; they now assert the effective configuration instead, which is what their comments describe. Note on urgency rather than severity: no released version has ever passed the property map to the connector, because the building method returned null until GROOVY-12119, which is in no GA release. There is therefore no installed base of connectors that believe they are authenticated. What makes this worth fixing before GA is that GROOVY-12119 leaves SSL working while authentication silently does not, which is a quieter failure than the wholly broken configuration it replaced. The default remains an unauthenticated connector when no authentication is requested. Warning on that is a separate question from this one.
1 parent 34ed575 commit b4b41b0

2 files changed

Lines changed: 152 additions & 9 deletions

File tree

subprojects/groovy-jmx/src/main/groovy/groovy/jmx/builder/JmxServerConnectorFactory.groovy

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package groovy.jmx.builder
2020

2121
import javax.management.MBeanServer
22+
import javax.management.remote.JMXAuthenticator
2223
import javax.management.remote.JMXConnectorServer
2324
import javax.management.remote.JMXConnectorServerFactory
2425
import javax.management.remote.JMXServiceURL
@@ -42,11 +43,19 @@ import javax.rmi.ssl.SslRMIServerSocketFactory
4243
* "authenticate":true|false,
4344
* "passwordFile":"...",
4445
* "accessFile":"...",
46+
* "loginConfig":"...",
4547
* "sslEnabled" : true | false
4648
* ...
4749
* ]
4850
* )
4951
* </pre>
52+
* <p>
53+
* When {@code authenticate} is true a source of credentials must be supplied, being one of
54+
* {@code passwordFile}, {@code loginConfig}, or a {@code jmx.remote.authenticator} entry
55+
* holding a {@link javax.management.remote.JMXAuthenticator}. A connector which was asked to
56+
* authenticate but has none of these would start open, so that combination is rejected rather
57+
* than accepted silently. Any other entry in {@code properties} is passed to the connector
58+
* environment unaltered.
5059
*
5160
* @see javax.management.remote.JMXConnectorServer
5261
*/
@@ -149,17 +158,36 @@ class JmxServerConnectorFactory extends AbstractFactory {
149158
if (!props) return null
150159
HashMap<String, Object> env = new HashMap<String, Object>()
151160

152-
// secure connection
161+
// Authentication. The com.sun.management.jmxremote.* names belong to the JDK's
162+
// out-of-the-box management agent, which translates them into the jmx.remote.x.*
163+
// names a connector server actually consumes (see sun.management.jmxremote.
164+
// ConnectorBootstrap). Nothing performs that translation here, so do it: putting the
165+
// agent's names into a connector environment leaves the connector with no
166+
// authenticator at all, and it accepts credential-less clients.
153167
def auth = props.remove("com.sun.management.jmxremote.authenticate") ?: props.remove("authenticate")
154-
env.put("com.sun.management.jmxremote.authenticate", auth)
155168
def pFile = props.remove("com.sun.management.jmxremote.password.file") ?: props.remove("passwordFile")
156-
env.put("com.sun.management.jmxremote.password.file", pFile)
157169
def aFile = props.remove("com.sun.management.jmxremote.access.file") ?: props.remove("accessFile")
158-
env.put("com.sun.management.jmxremote.access.file", aFile)
170+
def loginConfig = props.remove("com.sun.management.jmxremote.login.config") ?: props.remove("loginConfig")
171+
172+
if (Boolean.valueOf(auth?.toString())) {
173+
// A caller may instead pass a JMXAuthenticator straight through, which is the
174+
// standard JSR-160 route for custom authentication and is a credential source too.
175+
// Validate the value, not just the key: a present-but-null (or wrong-typed) entry is
176+
// not a credential source and would leave the connector unauthenticated.
177+
boolean suppliedAuthenticator = props.get(JMXConnectorServer.AUTHENTICATOR) instanceof JMXAuthenticator
178+
if (!pFile && !loginConfig && !suppliedAuthenticator) {
179+
throw new JmxBuilderException("Connector authentication was requested but no source " +
180+
"of credentials was provided; supply 'passwordFile', 'loginConfig' or a " +
181+
"'${JMXConnectorServer.AUTHENTICATOR}' entry, otherwise the connector would " +
182+
"start unauthenticated.")
183+
}
184+
if (pFile) env.put("jmx.remote.x.password.file", pFile)
185+
if (loginConfig) env.put("jmx.remote.x.login.config", loginConfig)
186+
if (aFile) env.put("jmx.remote.x.access.file", aFile)
187+
}
159188

160189
// SSL connection
161190
def ssl = props.remove("com.sun.management.jmxremote.ssl") ?: props.remove("sslEnabled")
162-
env.put("com.sun.management.jmxremote.ssl", ssl)
163191

164192
// config other rmi props
165193
if (protocol == "rmi") {

subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import org.junit.jupiter.api.BeforeEach
2323
import org.junit.jupiter.api.Test
2424
import org.junit.jupiter.api.extension.ExtendWith
2525

26+
import static groovy.test.GroovyAssert.shouldFail
27+
28+
import javax.management.remote.JMXAuthenticator
2629
import javax.management.remote.JMXConnector
30+
import javax.management.remote.JMXConnectorServer
2731
import javax.management.remote.JMXConnectorFactory
2832
import javax.management.remote.JMXServiceURL
2933
import javax.management.remote.rmi.RMIConnectorServer
@@ -48,6 +52,113 @@ class JmxServerConnectorFactoryTest {
4852
JmxConnectorHelper.destroyRmiRegistry(rmi.registry)
4953
}
5054

55+
// GROOVY-12270: authentication properties must land on the keys a connector server
56+
// consumes, not on the JDK management agent's names, which it ignores.
57+
@Test
58+
void testAuthenticationPropertiesMapToConsumedKeys() {
59+
def factory = new JmxServerConnectorFactory()
60+
def env = factory.confiConnectorProperties('rmi', rmi.port,
61+
[authenticate: true, passwordFile: 'pwd.properties', accessFile: 'access.properties'])
62+
63+
assert env['jmx.remote.x.password.file'] == 'pwd.properties'
64+
assert env['jmx.remote.x.access.file'] == 'access.properties'
65+
assert !env.containsKey('com.sun.management.jmxremote.password.file')
66+
assert !env.containsKey('com.sun.management.jmxremote.access.file')
67+
}
68+
69+
@Test
70+
void testLoginConfigMapsToConsumedKey() {
71+
def factory = new JmxServerConnectorFactory()
72+
def env = factory.confiConnectorProperties('rmi', rmi.port,
73+
[authenticate: true, loginConfig: 'MyLoginModule'])
74+
75+
assert env['jmx.remote.x.login.config'] == 'MyLoginModule'
76+
}
77+
78+
// Credentials are only configured when authentication was actually asked for.
79+
@Test
80+
void testCredentialsIgnoredWhenAuthenticationNotRequested() {
81+
def factory = new JmxServerConnectorFactory()
82+
def env = factory.confiConnectorProperties('rmi', rmi.port,
83+
[authenticate: false, passwordFile: 'pwd.properties'])
84+
85+
assert !env.containsKey('jmx.remote.x.password.file')
86+
}
87+
88+
// Asking for authentication without any source of credentials would start an open
89+
// connector, which is the failure this ticket is about, so it is rejected.
90+
@Test
91+
void testAuthenticationWithoutCredentialSourceIsRejected() {
92+
def factory = new JmxServerConnectorFactory()
93+
def ex = shouldFail(JmxBuilderException) {
94+
factory.confiConnectorProperties('rmi', rmi.port, [authenticate: true])
95+
}
96+
assert ex.message.contains('passwordFile')
97+
assert ex.message.contains('loginConfig')
98+
}
99+
100+
// A caller may supply their own JMXAuthenticator instead of a password file; that is the
101+
// standard JSR-160 route and must count as a source of credentials.
102+
@Test
103+
void testCallerSuppliedAuthenticatorSatisfiesAuthenticationRequest() {
104+
def factory = new JmxServerConnectorFactory()
105+
def authenticator = { env -> new javax.security.auth.Subject() } as JMXAuthenticator
106+
def env = factory.confiConnectorProperties('rmi', rmi.port,
107+
[authenticate: true, (JMXConnectorServer.AUTHENTICATOR): authenticator])
108+
109+
assert env[JMXConnectorServer.AUTHENTICATOR].is(authenticator)
110+
}
111+
112+
// A present-but-null (or wrong-typed) authenticator entry is not a credential source; it must
113+
// be rejected rather than pass the check on the key's presence alone and leave the connector
114+
// unauthenticated.
115+
@Test
116+
void testNullAuthenticatorIsNotACredentialSource() {
117+
def factory = new JmxServerConnectorFactory()
118+
def ex = shouldFail(JmxBuilderException) {
119+
factory.confiConnectorProperties('rmi', rmi.port,
120+
[authenticate: true, (JMXConnectorServer.AUTHENTICATOR): null])
121+
}
122+
assert ex.message.contains(JMXConnectorServer.AUTHENTICATOR)
123+
124+
shouldFail(JmxBuilderException) {
125+
factory.confiConnectorProperties('rmi', rmi.port,
126+
[authenticate: true, (JMXConnectorServer.AUTHENTICATOR): 'not-an-authenticator'])
127+
}
128+
}
129+
130+
// End-to-end: a connector configured to authenticate must reject a credential-less client.
131+
@Test
132+
void testAuthenticatedConnectorRejectsAnonymousClient() {
133+
File dir = File.createTempDir()
134+
try {
135+
File password = new File(dir, 'jmxremote.password')
136+
password.text = 'probeuser probepass\n'
137+
File access = new File(dir, 'jmxremote.access')
138+
access.text = 'probeuser readwrite\n'
139+
[password, access].each { it.setReadable(false, false); it.setReadable(true, true) }
140+
141+
def server = builder.serverConnector(port: rmi.port,
142+
properties: [authenticate: true, passwordFile: password.path, accessFile: access.path])
143+
server.start()
144+
try {
145+
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:${rmi.port}/jmxrmi")
146+
shouldFail(SecurityException) {
147+
JMXConnectorFactory.connect(url, null).withCloseable { it.MBeanServerConnection.MBeanCount }
148+
}
149+
// ...and accept the configured one.
150+
def creds = [(JMXConnector.CREDENTIALS): ['probeuser', 'probepass'] as String[]]
151+
JMXConnectorFactory.connect(url, creds).withCloseable {
152+
assert it.MBeanServerConnection.MBeanCount > 0
153+
}
154+
} finally {
155+
server.stop()
156+
}
157+
} finally {
158+
dir.deleteDir()
159+
}
160+
}
161+
51162
@Test
52163
void testJmxServerConnectorNode() {
53164
RMIConnectorServer result = builder.serverConnector(port: rmi.port)
@@ -81,8 +192,11 @@ class JmxServerConnectorFactoryTest {
81192
def env = factory.confiConnectorProperties('rmi', rmi.port, [authenticate: false])
82193

83194
assert env != null : 'connector environment map must not be discarded'
84-
// supplied/derived entries are present
85-
assert env.containsKey('com.sun.management.jmxremote.authenticate')
195+
// GROOVY-12270: the com.sun.management.jmxremote.* names belong to the JDK management
196+
// agent and mean nothing in a connector environment, so they are no longer copied into
197+
// it; authentication was not requested here, so nothing is configured for it.
198+
assert !env.containsKey('com.sun.management.jmxremote.authenticate')
199+
assert !env.containsKey('jmx.remote.x.password.file')
86200
}
87201

88202
// GROOVY-12119: when SSL is requested the env map must carry the SSL socket factories
@@ -92,7 +206,8 @@ class JmxServerConnectorFactoryTest {
92206
def env = factory.confiConnectorProperties('rmi', rmi.port, [sslEnabled: true])
93207

94208
assert env != null
95-
assert env['com.sun.management.jmxremote.ssl']
209+
// The socket factories are what actually enable SSL; see GROOVY-12270 for why the
210+
// com.sun.management.jmxremote.ssl key itself is no longer placed in the environment.
96211
assert env[RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE] instanceof SslRMIServerSocketFactory
97212
assert env[RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE] instanceof SslRMIClientSocketFactory
98213
}
@@ -127,7 +242,7 @@ class JmxServerConnectorFactoryTest {
127242
def env = factory.confiConnectorProperties('rmi', rmi.port, ['com.sun.management.jmxremote.ssl': true])
128243

129244
assert env != null
130-
assert env['com.sun.management.jmxremote.ssl']
245+
// Recognition is evidenced by the socket factories being configured from it.
131246
assert env[RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE] instanceof SslRMIServerSocketFactory
132247
}
133248

0 commit comments

Comments
 (0)