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 72306c04bef8a4cd901aaa86744dfbcddd9b1896 Author: Richard Zowalla <[email protected]> AuthorDate: Tue Oct 13 08:49:50 2026 +0200 Clear the WS-Security caller identity after the JAX-WS request (#3084) Every UsernameToken login of a message is undone on the thread which did it, also when the chain fails or resumes on another thread. (cherry picked from commit 9246fa872edf323caaa9cb2b890fc63b7ac39947) --- .../openejb/server/cxf/ConfigureCxfSecurity.java | 3 + .../apache/openejb/server/cxf/CxfWsContainer.java | 31 +- .../openejb/server/cxf/OpenEJBLoginValidator.java | 15 + .../server/cxf/WSSLoginCleanupInterceptor.java | 161 ++++++++ .../cxf/CxfWsContainerSecurityStateTest.java | 426 +++++++++++++++++++++ 5 files changed, 634 insertions(+), 2 deletions(-) diff --git a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/ConfigureCxfSecurity.java b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/ConfigureCxfSecurity.java index 599de67b1f..cbc895c08e 100644 --- a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/ConfigureCxfSecurity.java +++ b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/ConfigureCxfSecurity.java @@ -109,6 +109,9 @@ public class ConfigureCxfSecurity { // if WS Security is used with a JAX-WS handler (See EjbInterceptor), we have to deal with mustUnderstand flag // in WS Security headers. So, let's add an interceptor endpoint.getInInterceptors().add(new WSSPassThroughInterceptor()); + + // undo the logins of the message even when it fails + endpoint.getOutFaultInterceptors().add(WSSLoginCleanupInterceptor.OUT_FAULT); } if (null != outProps && !outProps.isEmpty()) { diff --git a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/CxfWsContainer.java b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/CxfWsContainer.java index 08e94ac542..44a3979bf1 100644 --- a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/CxfWsContainer.java +++ b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/CxfWsContainer.java @@ -22,11 +22,13 @@ import org.apache.cxf.transport.DestinationFactory; import org.apache.cxf.transport.http.AbstractHTTPDestination; import org.apache.openejb.assembler.classic.util.ServiceConfiguration; import org.apache.openejb.core.webservices.PortData; +import org.apache.openejb.loader.SystemInstance; import org.apache.openejb.monitoring.LocalMBeanServer; import org.apache.openejb.server.cxf.transport.util.CxfUtil; import org.apache.openejb.server.httpd.HttpListener; import org.apache.openejb.server.httpd.HttpRequest; import org.apache.openejb.server.httpd.HttpResponse; +import org.apache.openejb.spi.SecurityService; import javax.management.ObjectName; @@ -81,11 +83,36 @@ public abstract class CxfWsContainer implements HttpListener { public void onMessage(final HttpRequest request, final HttpResponse response) throws Exception { final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(CxfUtil.initBusLoader()); + final SecurityService securityService = SystemInstance.get().getComponent(SecurityService.class); + final Object securityState = securityService == null ? null : securityService.currentState(); try { destination.invoke(null, request.getServletContext(), request, response); } finally { - if (oldLoader != null) { - CxfUtil.clearBusLoader(oldLoader); + try { + if (securityService != null) { + restoreSecurityState(securityService, securityState, request); + } + } finally { + if (oldLoader != null) { + CxfUtil.clearBusLoader(oldLoader); + } + } + } + } + + // a ws-security login associates the caller with the current (pooled) thread, + // undo it once the request is done so it can't leak into the next one + private static void restoreSecurityState(final SecurityService securityService, final Object securityState, + final HttpRequest request) { + if (securityService.currentState() != securityState) { + securityService.setState(securityState); + } + + // logins done by this thread, the ones of a resumed chain are released by WSSLoginCleanupInterceptor + if (request.getAttribute(OpenEJBLoginValidator.LOGINS) instanceof WSSLoginCleanupInterceptor.Logins logins) { + logins.release(true); + if (logins.isEmpty()) { + request.removeAttribute(OpenEJBLoginValidator.LOGINS); } } } diff --git a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/OpenEJBLoginValidator.java b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/OpenEJBLoginValidator.java index 1c6fc77e17..854bdaa0fa 100644 --- a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/OpenEJBLoginValidator.java +++ b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/OpenEJBLoginValidator.java @@ -16,6 +16,8 @@ */ package org.apache.openejb.server.cxf; +import org.apache.cxf.message.Message; +import org.apache.cxf.transport.http.AbstractHTTPDestination; import org.apache.openejb.core.security.AbstractSecurityService; import org.apache.openejb.loader.SystemInstance; import org.apache.openejb.spi.SecurityService; @@ -25,10 +27,14 @@ import org.apache.wss4j.dom.handler.RequestData; import org.apache.wss4j.dom.message.token.UsernameToken; import org.apache.wss4j.dom.validate.UsernameTokenValidator; +import jakarta.servlet.http.HttpServletRequest; import javax.security.auth.callback.Callback; import javax.security.auth.login.LoginException; public class OpenEJBLoginValidator extends UsernameTokenValidator { + // request attribute holding the logins of the request, CxfWsContainer releases the ones of its thread + public static final String LOGINS = WSSLoginCleanupInterceptor.Logins.KEY; + @Override protected void verifyDigestPassword(final UsernameToken usernameToken, final RequestData data) throws WSSecurityException { @@ -47,6 +53,7 @@ public class OpenEJBLoginValidator extends UsernameTokenValidator { final String user = usernameToken.getName(); final String password = pwCb.getPassword(); final SecurityService securityService = SystemInstance.get().getComponent(SecurityService.class); + final Object previousState = securityService.currentState(); final Object token; try { securityService.disassociate(); @@ -58,5 +65,13 @@ public class OpenEJBLoginValidator extends UsernameTokenValidator { } catch (final LoginException e) { throw new SecurityException("cannot log user " + user, e); } + + // undo the login once the message is processed, a message can carry several tokens + if (data.getMsgContext() instanceof Message message) { + WSSLoginCleanupInterceptor.register(message, token, previousState, securityService.currentState()); + if (message.get(AbstractHTTPDestination.HTTP_REQUEST) instanceof HttpServletRequest request) { + request.setAttribute(LOGINS, WSSLoginCleanupInterceptor.Logins.of(message, false)); + } + } } } diff --git a/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/WSSLoginCleanupInterceptor.java b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/WSSLoginCleanupInterceptor.java new file mode 100644 index 0000000000..ae31f3f33a --- /dev/null +++ b/server/openejb-cxf/src/main/java/org/apache/openejb/server/cxf/WSSLoginCleanupInterceptor.java @@ -0,0 +1,161 @@ +/* + * 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.cxf; + +import org.apache.cxf.interceptor.InterceptorChain; +import org.apache.cxf.interceptor.OutgoingChainInterceptor; +import org.apache.cxf.message.Exchange; +import org.apache.cxf.message.Message; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; +import org.apache.openejb.loader.SystemInstance; +import org.apache.openejb.spi.SecurityService; + +import javax.security.auth.login.LoginException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Undoes the logins done by {@link OpenEJBLoginValidator} for a message once it has been processed: + * the caller identity is removed from the thread which did the login and every login token is logged out. + * It runs at the end of the in chain (after the response has been sent) and at the end of the out fault chain, + * so it doesn't depend on the thread which received the request (continuations can resume the chain elsewhere). + */ +public class WSSLoginCleanupInterceptor extends AbstractPhaseInterceptor<Message> { + public static final WSSLoginCleanupInterceptor IN = new WSSLoginCleanupInterceptor(Phase.POST_INVOKE); + public static final WSSLoginCleanupInterceptor OUT_FAULT = new WSSLoginCleanupInterceptor(Phase.SETUP_ENDING); + + public WSSLoginCleanupInterceptor(final String phase) { + super(WSSLoginCleanupInterceptor.class.getName() + "." + phase, phase); + if (Phase.POST_INVOKE.equals(phase)) { + addAfter(OutgoingChainInterceptor.class.getName()); + } + } + + @Override + public void handleMessage(final Message message) { + final Logins logins = Logins.of(message, false); + if (logins != null) { + logins.release(false); + } + } + + @Override + public void handleFault(final Message message) { + handleMessage(message); + } + + static void register(final Message message, final Object token, final Object previousState, final Object state) { + Logins.of(message, true).add(token, previousState, state); + + final InterceptorChain chain = message.getInterceptorChain(); + if (chain != null && !contains(chain)) { + chain.add(IN); + } + } + + private static boolean contains(final InterceptorChain chain) { + for (final Object interceptor : chain) { + if (interceptor == IN) { + return true; + } + } + return false; + } + + /** + * Logins done for a message, shared through the exchange so the fault chain sees them too. + */ + public static final class Logins { + public static final String KEY = Logins.class.getName(); + + private final List<Login> logins = new ArrayList<>(); + + static Logins of(final Message message, final boolean create) { + final Exchange exchange = message.getExchange(); + final Object existing = exchange != null ? exchange.get(KEY) : message.get(KEY); + if (existing instanceof Logins found) { + return found; + } + if (!create) { + return null; + } + final Logins logins = new Logins(); + if (exchange != null) { + exchange.put(KEY, logins); + } else { + message.put(KEY, logins); + } + return logins; + } + + synchronized void add(final Object token, final Object previousState, final Object state) { + logins.add(new Login(token, Thread.currentThread(), previousState, state)); + } + + public synchronized boolean isEmpty() { + return logins.isEmpty(); + } + + /** + * Restores the caller of the current thread and logs out the tokens. + * + * @param currentThreadOnly only release the logins done by the current thread, + * the other ones are released by their own thread or at the end of the chain + */ + public void release(final boolean currentThreadOnly) { + final List<Login> released = new ArrayList<>(); + final Thread current = Thread.currentThread(); + synchronized (this) { + for (final Iterator<Login> it = logins.iterator(); it.hasNext(); ) { + final Login login = it.next(); + if (!currentThreadOnly || login.thread == current) { + released.add(login); + it.remove(); + } + } + } + if (released.isEmpty()) { + return; + } + + final SecurityService securityService = SystemInstance.get().getComponent(SecurityService.class); + if (securityService == null) { + return; + } + + // latest first so the state before the first login wins + for (int i = released.size() - 1; i >= 0; i--) { + final Login login = released.get(i); + if (login.thread == current && securityService.currentState() == login.state) { + securityService.setState(login.previousState); + } + } + for (final Login login : released) { + try { + securityService.logout(login.token); + } catch (final LoginException e) { + // already logged out + } + } + } + } + + private record Login(Object token, Thread thread, Object previousState, Object state) { + } +} diff --git a/server/openejb-cxf/src/test/java/org/apache/openejb/server/cxf/CxfWsContainerSecurityStateTest.java b/server/openejb-cxf/src/test/java/org/apache/openejb/server/cxf/CxfWsContainerSecurityStateTest.java new file mode 100644 index 0000000000..272cfbdf4e --- /dev/null +++ b/server/openejb-cxf/src/test/java/org/apache/openejb/server/cxf/CxfWsContainerSecurityStateTest.java @@ -0,0 +1,426 @@ +/* + * 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.cxf; + +import org.apache.cxf.Bus; +import org.apache.cxf.bus.extension.ExtensionManagerBus; +import org.apache.cxf.bus.managers.PhaseManagerImpl; +import org.apache.cxf.continuations.SuspendedInvocationException; +import org.apache.cxf.interceptor.AbstractAttributedInterceptorProvider; +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.interceptor.InterceptorChain; +import org.apache.cxf.message.Exchange; +import org.apache.cxf.message.ExchangeImpl; +import org.apache.cxf.message.Message; +import org.apache.cxf.message.MessageImpl; +import org.apache.cxf.phase.AbstractPhaseInterceptor; +import org.apache.cxf.phase.Phase; +import org.apache.cxf.phase.PhaseInterceptorChain; +import org.apache.cxf.service.model.EndpointInfo; +import org.apache.cxf.transport.http.AbstractHTTPDestination; +import org.apache.cxf.transport.http.DestinationRegistryImpl; +import org.apache.openejb.core.security.AbstractSecurityService; +import org.apache.openejb.loader.SystemInstance; +import org.apache.openejb.server.cxf.transport.HttpDestination; +import org.apache.openejb.server.httpd.HttpRequest; +import org.apache.openejb.server.httpd.HttpResponse; +import org.apache.openejb.spi.SecurityService; +import org.apache.wss4j.common.ext.WSPasswordCallback; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.dom.WSConstants; +import org.apache.wss4j.dom.handler.RequestData; +import org.apache.wss4j.dom.message.token.UsernameToken; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.w3c.dom.Document; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import javax.management.ObjectName; +import javax.security.auth.callback.Callback; +import javax.security.auth.login.LoginException; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.lang.reflect.Proxy; +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class CxfWsContainerSecurityStateTest { + private TestSecurityService securityService; + private Bus bus; + + @Before + public void setUp() { + securityService = new TestSecurityService(); + SystemInstance.get().setComponent(SecurityService.class, securityService); + bus = new ExtensionManagerBus(); + } + + @After + public void tearDown() { + securityService.disassociate(); + securityService.destroyResource(); + bus.shutdown(true); + SystemInstance.reset(); + } + + @Test + public void callerIsClearedAfterRequest() throws Exception { + final AtomicReference<Principal> caller = new AtomicReference<>(); + final HttpRequest request = request(); + + container(new Invocation() { + @Override + public void invoke(final HttpServletRequest req) throws Exception { + login(req); + caller.set(securityService.getCallerPrincipal()); + } + }).onMessage(request, null); + + assertNotNull("caller should be associated during the invocation", caller.get()); + assertEquals("jonathan", caller.get().getName()); + assertNull(securityService.currentState()); + assertNull(request.getAttribute(OpenEJBLoginValidator.LOGINS)); + assertEquals(1, securityService.loggedOut.size()); + } + + @Test + public void callerIsClearedWhenInvocationFails() throws Exception { + try { + container(new Invocation() { + @Override + public void invoke(final HttpServletRequest req) throws Exception { + login(req); + throw new IOException("broken"); + } + }).onMessage(request(), null); + fail(); + } catch (final IOException expected) { + // ok + } + + assertNull(securityService.currentState()); + assertEquals(1, securityService.loggedOut.size()); + } + + @Test + public void previousStateIsRestored() throws Exception { + final UUID outer = securityService.login("outer", "outer"); + securityService.associate(outer); + final Object outerState = securityService.currentState(); + + container(new Invocation() { + @Override + public void invoke(final HttpServletRequest req) throws Exception { + login(req); + assertEquals("jonathan", securityService.getCallerPrincipal().getName()); + } + }).onMessage(request(), null); + + assertSame(outerState, securityService.currentState()); + assertEquals(1, securityService.loggedOut.size()); + } + + @Test + public void everyTokenOfTheRequestIsLoggedOut() throws Exception { + final HttpRequest request = request(); + + container(new Invocation() { + @Override + public void invoke(final HttpServletRequest req) throws Exception { + final Message message = message(req); + login(message); + login(message); + login(message); + } + }).onMessage(request, null); + + assertNull(securityService.currentState()); + assertEquals(3, securityService.loggedIn.size()); + assertEquals(securityService.loggedIn, securityService.loggedOut); + assertNull(request.getAttribute(OpenEJBLoginValidator.LOGINS)); + } + + @Test + public void chainReleasesItsLoginsWithoutContainer() { + final AtomicReference<Principal> caller = new AtomicReference<>(); + final PhaseInterceptorChain chain = inChain( + new Step(Phase.PRE_PROTOCOL, m -> { + login(m); + login(m); + }), + new Step(Phase.INVOKE, m -> caller.set(securityService.getCallerPrincipal()))); + + chain.doIntercept(message(null, chain)); + + assertEquals("jonathan", caller.get().getName()); + assertNull(securityService.currentState()); + assertEquals(2, securityService.loggedOut.size()); + assertEquals(securityService.loggedIn, securityService.loggedOut); + } + + @Test + public void resumedChainReleasesLoginsOnItsOwnThread() throws Exception { + final AtomicReference<Principal> caller = new AtomicReference<>(); + final AtomicReference<Object> stateAfterChain = new AtomicReference<>(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + final boolean[] suspended = {false}; + final PhaseInterceptorChain chain = inChain( + new Step(Phase.RECEIVE, m -> { + if (!suspended[0]) { + suspended[0] = true; + m.getInterceptorChain().suspend(); + } + }), + new Step(Phase.PRE_PROTOCOL, m -> login(m)), + new Step(Phase.INVOKE, m -> caller.set(securityService.getCallerPrincipal()))); + + try { + chain.doIntercept(message(null, chain)); + fail(); + } catch (final SuspendedInvocationException expected) { + // the destination keeps the message for the continuation + } + assertEquals(InterceptorChain.State.PAUSED, chain.getState()); + assertNull(caller.get()); + + // the continuation resumes the chain on another thread, outside of CxfWsContainer.onMessage + final Thread resumer = new Thread(() -> { + try { + chain.resume(); + stateAfterChain.set(securityService.currentState()); + } catch (final Throwable t) { + error.set(t); + } + }); + resumer.start(); + resumer.join(TimeUnit.MINUTES.toMillis(1)); + + assertNull(error.get()); + assertEquals("jonathan", caller.get().getName()); + assertNull("resuming thread must not keep the caller", stateAfterChain.get()); + assertEquals(1, securityService.loggedOut.size()); + assertEquals(securityService.loggedIn, securityService.loggedOut); + } + + @Test + public void faultChainReleasesLogins() { + final PhaseInterceptorChain outFault = new PhaseInterceptorChain(new PhaseManagerImpl().getOutPhases()); + outFault.add(WSSLoginCleanupInterceptor.OUT_FAULT); + + final AtomicReference<Object> stateAfterFault = new AtomicReference<>(); + final PhaseInterceptorChain chain = inChain( + new Step(Phase.PRE_PROTOCOL, m -> login(m)), + new Step(Phase.INVOKE, m -> { + throw new Fault(new IllegalStateException("broken")); + })); + chain.setFaultObserver(m -> { + final Message fault = new MessageImpl(); + fault.setExchange(m.getExchange()); + outFault.doIntercept(fault); + stateAfterFault.set(securityService.currentState()); + }); + + chain.doIntercept(message(null, chain)); + + assertNull(stateAfterFault.get()); + assertEquals(1, securityService.loggedOut.size()); + assertEquals(securityService.loggedIn, securityService.loggedOut); + } + + @Test + public void securedEndpointGetsFaultCleanup() { + final AbstractAttributedInterceptorProvider endpoint = new AbstractAttributedInterceptorProvider() { + }; + final Map<String, Object> in = new HashMap<>(); + in.put("action", "UsernameToken"); + ConfigureCxfSecurity.setupWSS4JChain(endpoint, in, null); + + assertTrue(endpoint.getOutFaultInterceptors().contains(WSSLoginCleanupInterceptor.OUT_FAULT)); + } + + private PhaseInterceptorChain inChain(final Step... steps) { + final PhaseInterceptorChain chain = new PhaseInterceptorChain(new PhaseManagerImpl().getInPhases()); + for (final Step step : steps) { + chain.add(step); + } + return chain; + } + + private static Message message(final HttpServletRequest req) { + return message(req, null); + } + + private static Message message(final HttpServletRequest req, final InterceptorChain chain) { + final Message message = new MessageImpl(); + message.setInterceptorChain(chain); + final Exchange exchange = new ExchangeImpl(); + exchange.setInMessage(message); + if (req != null) { + message.put(AbstractHTTPDestination.HTTP_REQUEST, req); + } + return message; + } + + private void login(final HttpServletRequest req) throws Exception { + login(message(req)); + } + + private void login(final Message message) { + final Document doc; + try { + doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + } catch (final ParserConfigurationException e) { + throw new IllegalStateException(e); + } + final UsernameToken token = new UsernameToken(false, doc, WSConstants.PASSWORD_DIGEST); + token.setName("jonathan"); + token.addNonce(doc); + token.addCreated(false, doc); + token.setPassword("secret"); + + final RequestData data = new RequestData(); + data.setMsgContext(message); + data.setCallbackHandler(callbacks -> { + for (final Callback callback : callbacks) { + if (callback instanceof WSPasswordCallback pwd && "jonathan".equals(pwd.getIdentifier())) { + pwd.setPassword("secret"); + } + } + }); + + try { + new OpenEJBLoginValidator().verifyDigestPassword(token, data); + } catch (final WSSecurityException e) { + throw new IllegalStateException(e); + } + } + + private CxfWsContainer container(final Invocation invocation) throws IOException { + final EndpointInfo endpointInfo = new EndpointInfo(); + endpointInfo.setAddress("http://localhost/ws"); + + final CxfWsContainer container = new CxfWsContainer(bus, null, null, null) { + @Override + protected CxfEndpoint createEndpoint() { + return null; + } + + @Override + protected ObjectName registerMBean() { + return null; + } + + @Override + protected void setWsldUrl(final String wsdl) { + // no-op + } + }; + container.destination = new HttpDestination(bus, new DestinationRegistryImpl(), endpointInfo, "/ws") { + @Override + public void invoke(final ServletConfig config, final ServletContext context, + final HttpServletRequest req, final HttpServletResponse resp) throws IOException { + try { + invocation.invoke(req); + } catch (final IOException | RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + }; + return container; + } + + private static HttpRequest request() { + final Map<String, Object> attributes = new HashMap<>(); + return (HttpRequest) Proxy.newProxyInstance(CxfWsContainerSecurityStateTest.class.getClassLoader(), + new Class<?>[]{HttpRequest.class}, (proxy, method, args) -> { + switch (method.getName()) { + case "getAttribute": + return attributes.get(args[0]); + case "setAttribute": + attributes.put((String) args[0], args[1]); + return null; + case "removeAttribute": + attributes.remove(args[0]); + return null; + default: + return null; + } + }); + } + + private static final class Step extends AbstractPhaseInterceptor<Message> { + private final Consumer<Message> action; + + private Step(final String phase, final Consumer<Message> action) { + super(UUID.randomUUID().toString(), phase); + this.action = action; + } + + @Override + public void handleMessage(final Message message) { + action.accept(message); + } + } + + private interface Invocation { + void invoke(HttpServletRequest req) throws Exception; + } + + private static class TestSecurityService extends AbstractSecurityService { + private final List<UUID> loggedIn = new CopyOnWriteArrayList<>(); + private final List<UUID> loggedOut = new CopyOnWriteArrayList<>(); + + @Override + public UUID login(final String securityRealm, final String user, final String pass) throws LoginException { + if (!user.equals(pass) && !"secret".equals(pass)) { + throw new LoginException("bad password"); + } + final UUID token = registerSubject(createSubject(user, user)); + if (!"outer".equals(user)) { + loggedIn.add(token); + } + return token; + } + + @Override + public void logout(final UUID securityIdentity) throws LoginException { + super.logout(securityIdentity); + loggedOut.add(securityIdentity); + } + } +}
