This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-10.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 6792dc184c6b04f002467c16df639b1d32bce652 Author: Markus Jung <[email protected]> AuthorDate: Sat Sep 5 09:49:44 2026 +0200 disable remote datasource lookup by default (cherry picked from commit 6a5b442c9cf0d69cfd9e695b989a09aa79a878b1) --- docs/ejbd-transport.adoc | 26 ++++ docs/properties-listing.adoc | 10 ++ .../openejb/server/ejbd/JndiRequestHandler.java | 6 + .../apache/openejb/server/ejbd/AppClientTest.java | 99 ++++++++----- .../server/ejbd/RemoteDataSourceLookupTest.java | 157 +++++++++++++++++++++ 5 files changed, 259 insertions(+), 39 deletions(-) diff --git a/docs/ejbd-transport.adoc b/docs/ejbd-transport.adoc index 23ece1ae65..173ec1bb0f 100644 --- a/docs/ejbd-transport.adoc +++ b/docs/ejbd-transport.adoc @@ -41,6 +41,32 @@ servlet definition in your `web.xml` and set the url mapping to what you want (let's say /foo/*). Then use the provider url http://<host>:<port>/<webapp context name>/foo +== Remote datasource lookup + +Remote datasource lookup is disabled by default. Remote EJB invocations and +server-side datasource use are unaffected. Clients that previously looked up a +datasource remotely now receive a naming exception instead of connection details. + +Administrators who need this feature can set the following server system property +(for example in `conf/system.properties`): + +[source,properties] +---- +openejb.ejbd.datasource-metadata = true +---- + +This restores remote datasource lookup, including datasource references. The +client receives connection details, including database credentials, and opens +its own JDBC connection. This is a server-wide opt-in, not a per-user or +per-datasource permission. EJB method authorization does not restrict these +lookups. Enable it only when all clients able to reach the remote endpoint are +trusted to receive the exposed database credentials. + +The setting applies to both native EJBd and EJBd over HTTP/HTTPS. HTTPS protects +the connection in transit; it does not prevent the client from receiving the +credentials. Applications that only need remote business operations should +continue using remote EJB methods with datasource access performed on the server. + == Remote communication and serialization Remotely calling EJBs, independent of using Ejbd or other RMI/IIOP based diff --git a/docs/properties-listing.adoc b/docs/properties-listing.adoc index 1821d3ae44..65b51d5ef2 100644 --- a/docs/properties-listing.adoc +++ b/docs/properties-listing.adoc @@ -19,6 +19,16 @@ bool activate or not the remote services when available +openejb.ejbd.datasource-metadata + +boolean (default: false) + +Allow remote datasource lookup over EJBd, including HTTP/HTTPS. Enabling this +shares database connection details, including credentials, with clients able to +reach the endpoint. Applies server-wide, including datasource references; it +does not add per-user authorization. See link:ejbd-transport.html[Ejbd Transport] +for compatibility and configuration details. + .bind, <service prefix>.port, <service prefix>.disabled, <service prefix>.threads diff --git a/server/openejb-ejbd/src/main/java/org/apache/openejb/server/ejbd/JndiRequestHandler.java b/server/openejb-ejbd/src/main/java/org/apache/openejb/server/ejbd/JndiRequestHandler.java index dff0fb1f8d..671fe74831 100644 --- a/server/openejb-ejbd/src/main/java/org/apache/openejb/server/ejbd/JndiRequestHandler.java +++ b/server/openejb-ejbd/src/main/java/org/apache/openejb/server/ejbd/JndiRequestHandler.java @@ -270,6 +270,12 @@ class JndiRequestHandler extends RequestHandler { } else if (object == null) { throw new NullPointerException("lookup of '" + name + "' returned null"); } else if (object instanceof DataSource) { + // Both connection metadata and references can expose database credentials. + if (!SystemInstance.get().getOptions().get("openejb.ejbd.datasource-metadata", false)) { + throw new NamingException("Remote DataSource lookup is disabled. An administrator can enable it with " + + "openejb.ejbd.datasource-metadata=true to share database connection details, including credentials, " + + "with remote clients."); + } if (DataSourceFactory.knows(object)) { try { final DbcpDataSource cf = new DbcpDataSource(object); diff --git a/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/AppClientTest.java b/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/AppClientTest.java index 1adfa66611..34b7412dc3 100644 --- a/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/AppClientTest.java +++ b/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/AppClientTest.java @@ -41,6 +41,7 @@ import jakarta.ejb.Remote; import jakarta.ejb.RemoteHome; import javax.naming.Context; import javax.naming.InitialContext; +import javax.naming.NamingException; import javax.sql.DataSource; import java.rmi.RemoteException; import java.util.Properties; @@ -51,10 +52,19 @@ import java.util.Properties; public class AppClientTest extends TestCase { public void test() throws Exception { + checkClient(true); + } + + public void testWithoutRemoteDataSources() throws Exception { + checkClient(false); + } + + private void checkClient(final boolean remoteDataSources) throws Exception { final EjbServer ejbServer = new EjbServer(); final Properties initProps = new Properties(); + initProps.setProperty("openejb.ejbd.datasource-metadata", Boolean.toString(remoteDataSources)); initProps.setProperty("openejb.deployments.classpath.include", ""); initProps.setProperty("openejb.deployments.classpath.filter.descriptors", "true"); OpenEJB.init(initProps, new ServerFederation()); @@ -64,60 +74,71 @@ public class AppClientTest extends TestCase { final ServiceDaemon serviceDaemon = new ServiceDaemon(pool, 0, "localhost"); serviceDaemon.start(); - int port = serviceDaemon.getPort(); - - final Assembler assembler = SystemInstance.get().getComponent(Assembler.class); - final ConfigurationFactory config = new ConfigurationFactory(); + try { + int port = serviceDaemon.getPort(); - final EjbModule ejbModule = new EjbModule(new EjbJar("testejbmodule"), new OpenejbJar()); - final EjbJar ejbJar = ejbModule.getEjbJar(); - ejbJar.addEnterpriseBean(new StatelessBean(Orange.class)); + final Assembler assembler = SystemInstance.get().getComponent(Assembler.class); + final ConfigurationFactory config = new ConfigurationFactory(); - final ClassLoader loader = this.getClass().getClassLoader(); + final EjbModule ejbModule = new EjbModule(new EjbJar("testejbmodule"), new OpenejbJar()); + final EjbJar ejbJar = ejbModule.getEjbJar(); + ejbJar.addEnterpriseBean(new StatelessBean(Orange.class)); - final ClientModule clientModule = new ClientModule(new ApplicationClient(), loader, "orange-client", OrangeAppClient.class.getName(), "orange-client"); + final ClassLoader loader = this.getClass().getClassLoader(); - final AppModule appModule = new AppModule(loader, "testapp"); + final ClientModule clientModule = new ClientModule(new ApplicationClient(), loader, "orange-client", OrangeAppClient.class.getName(), "orange-client"); - appModule.getClientModules().add(clientModule); - appModule.getEjbModules().add(ejbModule); + final AppModule appModule = new AppModule(loader, "testapp"); - assembler.createApplication(config.configureApplication(appModule)); + appModule.getClientModules().add(clientModule); + appModule.getEjbModules().add(ejbModule); - final Properties props = new Properties(); - props.put("java.naming.factory.initial", "org.apache.openejb.client.RemoteInitialContextFactory"); - props.put("java.naming.provider.url", "ejbd://127.0.0.1:" + port); - props.put("openejb.client.moduleId", "orange-client"); + assembler.createApplication(config.configureApplication(appModule)); - Context context = new InitialContext(props); + final Properties props = new Properties(); + props.put("java.naming.factory.initial", "org.apache.openejb.client.RemoteInitialContextFactory"); + props.put("java.naming.provider.url", "ejbd://127.0.0.1:" + port); + props.put("openejb.client.moduleId", "orange-client"); - final Object home = context.lookup("comp/env/home"); - assertTrue(home instanceof OrangeHome); + Context context = new InitialContext(props); - OrangeHome orangeHome = (OrangeHome) home; - final OrangeRemote orangeRemote = orangeHome.create(); - assertEquals("bat", orangeRemote.echo("tab")); + final Object home = context.lookup("comp/env/home"); + assertTrue(home instanceof OrangeHome); - final Object business = context.lookup("comp/env/business"); - assertTrue(business instanceof OrangeBusinessRemote); - OrangeBusinessRemote orangeBusinessRemote = (OrangeBusinessRemote) business; - assertEquals("nap", orangeBusinessRemote.echo("pan")); + OrangeHome orangeHome = (OrangeHome) home; + final OrangeRemote orangeRemote = orangeHome.create(); + assertEquals("bat", orangeRemote.echo("tab")); - final Object dataSourceObject = context.lookup("comp/env/datasource"); - assertTrue(dataSourceObject instanceof DataSource); - // DataSource dataSource = (DataSource) dataSourceObject; - // assertEquals("nap", orangeBusinessRemote.echo("pan")); + final Object business = context.lookup("comp/env/business"); + assertTrue(business instanceof OrangeBusinessRemote); + OrangeBusinessRemote orangeBusinessRemote = (OrangeBusinessRemote) business; + assertEquals("nap", orangeBusinessRemote.echo("pan")); - props.put("openejb.client.moduleId", "openejb/global"); - context = new InitialContext(props); + if (remoteDataSources) { + assertTrue(context.lookup("comp/env/datasource") instanceof DataSource); + } else { + try { + context.lookup("comp/env/datasource"); + fail("Remote datasource lookup should be disabled"); + } catch (final NamingException expected) { + assertTrue(expected.getMessage().contains("Remote DataSource lookup is disabled")); + } + } - final Object global = context.lookup("global/testapp/testejbmodule/Orange!" + OrangeBusinessRemote.class.getName()); - assertTrue(global instanceof OrangeBusinessRemote); - OrangeBusinessRemote globalOrangeBusinessRemote = (OrangeBusinessRemote) global; - assertEquals("nap", globalOrangeBusinessRemote.echo("pan")); + props.put("openejb.client.moduleId", "openejb/global"); + context = new InitialContext(props); - serviceDaemon.stop(); - OpenEJB.destroy(); + final Object global = context.lookup("global/testapp/testejbmodule/Orange!" + OrangeBusinessRemote.class.getName()); + assertTrue(global instanceof OrangeBusinessRemote); + OrangeBusinessRemote globalOrangeBusinessRemote = (OrangeBusinessRemote) global; + assertEquals("nap", globalOrangeBusinessRemote.echo("pan")); + } finally { + try { + serviceDaemon.stop(); + } finally { + OpenEJB.destroy(); + } + } } public static interface OrangeHome extends EJBHome { diff --git a/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/RemoteDataSourceLookupTest.java b/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/RemoteDataSourceLookupTest.java new file mode 100644 index 0000000000..9f440f7811 --- /dev/null +++ b/server/openejb-ejbd/src/test/java/org/apache/openejb/server/ejbd/RemoteDataSourceLookupTest.java @@ -0,0 +1,157 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.openejb.server.ejbd; + +import org.apache.openejb.OpenEJB; +import org.apache.openejb.client.Client; +import org.apache.openejb.client.DataSourceMetaData; +import org.apache.openejb.client.JNDIRequest; +import org.apache.openejb.client.JNDIResponse; +import org.apache.openejb.client.RequestMethodCode; +import org.apache.openejb.client.ResponseCodes; +import org.apache.openejb.client.ServerMetaData; +import org.apache.openejb.core.ServerFederation; +import org.apache.openejb.loader.SystemInstance; +import org.apache.openejb.server.ServiceDaemon; +import org.apache.openejb.spi.ContainerSystem; +import org.junit.Test; + +import jakarta.resource.Referenceable; +import javax.naming.Context; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import javax.sql.DataSource; +import java.lang.reflect.Proxy; +import java.net.URI; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class RemoteDataSourceLookupTest { + private static final String OPTION = "openejb.ejbd.datasource-metadata"; + private static final String PASSWORD = "synthetic-datasource-password"; + + @Test + public void disabledByDefault() throws Exception { + checkLookup(null); + } + + @Test + public void explicitlyDisabled() throws Exception { + checkLookup("false"); + } + + @Test + public void explicitlyEnabled() throws Exception { + checkLookup("true"); + } + + private void checkLookup(final String option) throws Exception { + final Properties properties = new Properties(); + properties.setProperty("openejb.deployments.classpath", "false"); + properties.setProperty("openejb.jdbc.datasource-creator", "dbcp"); + properties.setProperty("LookupDS", "new://Resource?type=DataSource"); + properties.setProperty("LookupDS.JdbcDriver", "org.hsqldb.jdbcDriver"); + properties.setProperty("LookupDS.JdbcUrl", "jdbc:hsqldb:mem:lookup-test"); + properties.setProperty("LookupDS.UserName", "lookup-user"); + properties.setProperty("LookupDS.Password", PASSWORD); + properties.setProperty("LookupDS.InitialSize", "0"); + properties.setProperty("LookupDS.JtaManaged", "false"); + if (option != null) { + properties.setProperty(OPTION, option); + } + + ServiceDaemon daemon = null; + try { + OpenEJB.init(properties, new ServerFederation()); + final Context local = SystemInstance.get().getComponent(ContainerSystem.class).getJNDIContext(); + final Object managed = local.lookup("openejb/Resource/LookupDS"); + assertTrue(managed instanceof DataSource); + // Exercise both the server resource fallback and an application-client binding. + local.bind("openejb/client/lookup-client/comp/env/jdbc", managed); + final AtomicBoolean referenceRead = new AtomicBoolean(); + final DataSource referenceDataSource = (DataSource) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class<?>[]{DataSource.class, Referenceable.class}, (proxy, method, args) -> { + if ("getReference".equals(method.getName())) { + referenceRead.set(true); + final Reference reference = new Reference(DataSource.class.getName()); + reference.add(new StringRefAddr("password", PASSWORD)); + return reference; + } + if ("toString".equals(method.getName())) { + return "ReferenceDataSource"; + } + if ("hashCode".equals(method.getName())) { + return System.identityHashCode(proxy); + } + if ("equals".equals(method.getName())) { + return proxy == args[0]; + } + throw new UnsupportedOperationException(method.getName()); + }); + local.bind("openejb/Resource/ReferenceDS", referenceDataSource); + + final EjbServer server = new EjbServer(); + server.init(new Properties()); + daemon = new ServiceDaemon(server, 0, "127.0.0.1"); + daemon.start(); + final ServerMetaData remote = new ServerMetaData(new URI("ejbd://127.0.0.1:" + daemon.getPort())); + + // No authentication request is sent: the server must enforce the option itself. + final JNDIResponse resource = lookup(remote, "LookupDS", null); + final JNDIResponse application = lookup(remote, "comp/env/jdbc", "lookup-client"); + final JNDIResponse reference = lookup(remote, "ReferenceDS", null); + if ("true".equals(option)) { + for (final JNDIResponse response : new JNDIResponse[]{resource, application}) { + assertEquals(ResponseCodes.JNDI_DATA_SOURCE, response.getResponseCode()); + final DataSourceMetaData metadata = (DataSourceMetaData) response.getResult(); + assertEquals("lookup-user", metadata.getDefaultUserName()); + assertEquals(PASSWORD, metadata.getDefaultPassword()); + } + assertEquals(String.valueOf(reference.getResult()), ResponseCodes.JNDI_REFERENCE, reference.getResponseCode()); + assertEquals(PASSWORD, ((Reference) reference.getResult()).get("password").getContent()); + assertTrue(referenceRead.get()); + } else { + for (final JNDIResponse response : new JNDIResponse[]{resource, application, reference}) { + assertEquals(ResponseCodes.JNDI_NAMING_EXCEPTION, response.getResponseCode()); + } + assertFalse(referenceRead.get()); + } + assertEquals(ResponseCodes.JNDI_NOT_FOUND, lookup(remote, "MissingLookupDS", null).getResponseCode()); + assertTrue(local.lookup("openejb/Resource/LookupDS") instanceof DataSource); + } finally { + try { + if (daemon != null) { + daemon.stop(); + } + } finally { + OpenEJB.destroy(); + } + } + } + + private JNDIResponse lookup(final ServerMetaData server, final String name, final String module) throws Exception { + final JNDIRequest request = new JNDIRequest(RequestMethodCode.JNDI_LOOKUP, name); + request.setModuleId(module); + final JNDIResponse response = new JNDIResponse(); + Client.request(request, response, server); + return response; + } +}
