Skip to content

TOMEE-4654 - Make the component naming context read-only by default#2846

Open
jungm wants to merge 1 commit into
mainfrom
claude/tomee-4654-fix-6ad09c
Open

TOMEE-4654 - Make the component naming context read-only by default#2846
jungm wants to merge 1 commit into
mainfrom
claude/tomee-4654-fix-6ad09c

Conversation

@jungm

@jungm jungm commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

Makes the java:comp component naming context read-only by default, as the Enterprise Beans spec (10.4.4) and EE.5.3.4 require: write attempts against a deployed application's ENC must not take effect.

Why

The IvmContext read-only enforcement already existed and was correct — checkReadOnly() throws OperationNotSupportedException and setReadOnly() cascades through the whole name tree. But nothing ever turned it on: openejb.forceReadOnlyAppNamingContext defaulted to false, so every deployed application got a fully writable java:comp, letting bind/rebind/rename through where the spec requires them refused.

Changes

  • Assembler: default openejb.forceReadOnlyAppNamingContext to true (kept as an explicit opt-out for anyone depending on the old writable behaviour).
  • Assembler.setAppNamingContextReadOnly: also mark the WebContext and AppContext naming contexts read-only. Servlets and JSF managed beans resolve java:comp/java:module/java:app through those contexts rather than through a BeanContext, so marking only the BeanContexts left the web tier writable. The one late write to the app context (app/BeanManager) happens before this point, so it is unaffected.
  • JavaCompReadOnlyTest (new): deploys a real application and asserts bind/rebind/rename/unbind/createSubcontext/destroySubcontext are all refused on java:comp and java:app, that nothing written becomes observable, and that pre-existing bindings survive.
  • AppNamingReadOnlyTest: inverted testAppNamingContextWritableByDefault (it asserted the exact bug) into a read-only-by-default test, plus a separate test for the opt-out path.

Verification

  • openejb-core full suite: 4093 tests, the only 6 failures are pre-existing security-test failures reproduced on a clean main (unrelated to this change).
  • Jakarta EE 11 Web Profile TCK enterprise-beans-30 partition against a freshly built Plume distribution: the naming/context write assertions now pass in both the EJB and web (servlet / filtered-servlet / JSF) vehicles.

Reviewer notes

  • This is a container-wide default change. An application that writes to its own ENC after deployment would now be refused; the property above restores the old behaviour if needed.
  • The TCK harness change to un-exclude these tests lives in the separate apache/tomee-tck repo and is not part of this PR.
  • The TCK run surfaced a separate, pre-existing read-side bug (filed as TOMEE-4658): in web components TomEE hands out Tomcat's org.apache.naming.NamingContext instead of IvmContext, so java:comp/env lists extra comp/module entries and close() fails. It is orthogonal to this read-only fix and not addressed here.

🤖 Generated with Claude Code

The Enterprise Beans spec (10.4.4) and EE.5.3.4 require java:comp and its
subcontexts to be read-only inside a deployed application: write attempts must
not take effect. The IvmContext read-only machinery already existed but was
gated behind openejb.forceReadOnlyAppNamingContext, which defaulted to false,
so every deployed application received a fully writable ENC.

Flip that default to true (retained as an explicit opt-out for backward
compatibility) and extend the enforcement to the web and app contexts. Servlets
and JSF managed beans resolve java:comp/java:module/java:app through the
WebContext and AppContext rather than a BeanContext, so marking only the
BeanContexts read-only left the web tier writable.

Adds JavaCompReadOnlyTest, which deploys a real application and asserts that
bind/rebind/rename/unbind/createSubcontext/destroySubcontext are all refused on
java:comp and java:app, and inverts AppNamingReadOnlyTest, whose former
testAppNamingContextWritableByDefault asserted the exact behaviour being fixed.
@rzo1

rzo1 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The underlying bug is real and worth fixing — the read-only machinery has been there since
d5b3b93 (2017) and nothing ever enabled it, so every ENC has been writable in violation
of EE.5.3.4 / Enterprise Beans 10.4.4. I applied the patch and confirmed JavaCompReadOnlyTest
fails on unpatched main with bind should have been refused and passes with the fix, and that
the read-only cascade doesn't leak into the container root context or the per-app global/module
contexts.

I can't approve as-is though — the marking happens at the wrong point in the lifecycle, and it
cuts both ways:

Container-internal binds can now fail. Assembler.isSkip (:1634-1645) skips every webapp
EjbJarInfo when the app is not webAppAlone. So for an EAR containing a WAR, that WAR's
EJB/managed-bean module is deployed later, by TomcatWebAppBuilder.startInternal via
assembler.initEjbs (:1453) and startEjbs (:1468) — i.e. after createApplication has already
marked appContext.getAppJndiContext() read-only. initEjbs -> jndiBuilder.build ->
JndiBuilder.bindJava does appContext.bind("app/" + moduleName + beanName, ref)
(JndiBuilder:694, :722), outside the NameAlreadyBoundException catch, and the caller wraps
NamingException into OpenEJBRuntimeException (:443).

I probed this directly in openejb-core: with the patch applied, after createApplication returns,
app1.getAppJndiContext().bind("app/mod3/SomeBean", ref) fails with
javax.naming.OperationNotSupportedException. I couldn't run a full EAR deployment here, so the
end-to-end failure is inferred from the call chain — but the refusal itself is proven and the
call chain is unconditional. Reload of such a WAR re-enters the same path
(TomcatWebAppBuilder:2087 only destroys the app when isUnDeployable/webAppAlone).

And the same modules never get marked. The flip side: setAppNamingContextReadOnly only
iterates allDeployments, the BeanContexts built during createApplication. The webapp modules
excluded by isSkip get their BeanContexts created afterwards, so their java:comp stays fully
writable — the spec violation you're targeting survives for exactly the EJBs that live in an EAR's
web modules.

Both problems have one shape of fix: record the read-only intent on the AppContext and apply it
where the BeanContexts are actually created (end of initEjbs/startEjbs), so late modules inherit
it and the container's own binds all run before the flag takes effect.

Other things:

  • The new WebContext loop is dead code. Neither TomcatWebAppBuilder nor
    LightweightWebAppBuilder stores an IvmContext or ContextHandler in WebContext.jndiEnc, so
    markReadOnly(webContext.getJndiEnc()) never matches either branch. That means the PR body's
    claim that this is what fixed the web vehicles isn't supported by the code, and web components
    still get a writable ENC.

  • arquillian-tomee-embedded's EmbeddedTomEEContainerTest.testEjbCanCreateSubContextByDefault
    (:72-88) still asserts the pre-PR semantic. It needs to be inverted in this PR, and honestly a
    container-wide default flip with this blast radius shouldn't land with only openejb-core unit
    coverage — an Arquillian test against a real TomEE would be worth it.

  • The opt-out is read from SystemInstance only, never from appInfo.properties. Most other
    Assembler switches support both (e.g. OPENEJB_TIMERS_ON at :1501/:1560 reads
    appInfo.properties.getProperty(..., globalDefault)). As written, one legacy app that writes into
    its ENC forces the whole container off the spec-required behaviour. Reading appInfo.properties
    first with the system property as fallback keeps it per-application.

  • assertWriteRefused hard-requires OperationNotSupportedException, which couples the new test to
    openejb.jndiExceptionOnFailedWrite — the createSubcontext case right below it correctly
    tolerates both outcomes. Make them consistent.

  • Minor test things: the rename/destroySubcontext assertions are tautological as written;
    deploy() runs outside the try/finally so a failure there skips SystemInstance.reset() and
    pollutes the next test.

  • "true".equals(SystemInstance.get().getProperty(FORCE_READ_ONLY_APP_NAMING, "true")) is
    case-sensitive, so -Dopenejb.forceReadOnlyAppNamingContext=FALSE silently keeps read-only on.
    Now that this is the opt-out for a spec-behaviour change, that matters more than it did.

  • if(/for( without the space doesn't match the file's convention. Pre-existing in the lines you
    moved, but the new markReadOnly helper is new code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants