Support Deferred Contexts

Closes gh-11817
Issue gh-10913
This commit is contained in:
Josh Cummings 2022-09-12 16:47:15 -06:00
parent 93250013e4
commit f054505d6d
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
4 changed files with 176 additions and 24 deletions

View File

@ -18,6 +18,8 @@ package org.springframework.security.core.context;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -127,9 +129,9 @@ public final class ListeningSecurityContextHolderStrategy implements SecurityCon
*/ */
@Override @Override
public void clearContext() { public void clearContext() {
SecurityContext from = getContext(); Supplier<SecurityContext> deferred = this.delegate.getDeferredContext();
this.delegate.clearContext(); this.delegate.clearContext();
publish(from, null); publish(new SecurityContextChangedEvent(deferred, SecurityContextChangedEvent.NO_CONTEXT));
} }
/** /**
@ -140,14 +142,28 @@ public final class ListeningSecurityContextHolderStrategy implements SecurityCon
return this.delegate.getContext(); return this.delegate.getContext();
} }
/**
* {@inheritDoc}
*/
@Override
public Supplier<SecurityContext> getDeferredContext() {
return this.delegate.getDeferredContext();
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void setContext(SecurityContext context) { public void setContext(SecurityContext context) {
SecurityContext from = getContext(); setDeferredContext(() -> context);
this.delegate.setContext(context); }
publish(from, context);
/**
* {@inheritDoc}
*/
@Override
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
this.delegate.setDeferredContext(new PublishOnceSupplier(getDeferredContext(), deferredContext));
} }
/** /**
@ -158,14 +174,42 @@ public final class ListeningSecurityContextHolderStrategy implements SecurityCon
return this.delegate.createEmptyContext(); return this.delegate.createEmptyContext();
} }
private void publish(SecurityContext previous, SecurityContext current) { private void publish(SecurityContextChangedEvent event) {
if (previous == current) {
return;
}
SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current);
for (SecurityContextChangedListener listener : this.listeners) { for (SecurityContextChangedListener listener : this.listeners) {
listener.securityContextChanged(event); listener.securityContextChanged(event);
} }
} }
class PublishOnceSupplier implements Supplier<SecurityContext> {
private final AtomicBoolean isPublished = new AtomicBoolean(false);
private final Supplier<SecurityContext> old;
private final Supplier<SecurityContext> updated;
PublishOnceSupplier(Supplier<SecurityContext> old, Supplier<SecurityContext> updated) {
if (old instanceof PublishOnceSupplier) {
this.old = ((PublishOnceSupplier) old).updated;
}
else {
this.old = old;
}
this.updated = updated;
}
@Override
public SecurityContext get() {
SecurityContext updated = this.updated.get();
if (this.isPublished.compareAndSet(false, true)) {
SecurityContext old = this.old.get();
if (old != updated) {
publish(new SecurityContextChangedEvent(old, updated));
}
}
return updated;
}
}
} }

View File

@ -16,6 +16,8 @@
package org.springframework.security.core.context; package org.springframework.security.core.context;
import java.util.function.Supplier;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
/** /**
@ -26,9 +28,24 @@ import org.springframework.context.ApplicationEvent;
*/ */
public class SecurityContextChangedEvent extends ApplicationEvent { public class SecurityContextChangedEvent extends ApplicationEvent {
private final SecurityContext oldContext; public static final Supplier<SecurityContext> NO_CONTEXT = () -> null;
private final SecurityContext newContext; private final Supplier<SecurityContext> oldContext;
private final Supplier<SecurityContext> newContext;
/**
* Construct an event
* @param oldContext the old security context
* @param newContext the new security context, use
* {@link SecurityContextChangedEvent#NO_CONTEXT} for if the context is cleared
* @since 5.8
*/
public SecurityContextChangedEvent(Supplier<SecurityContext> oldContext, Supplier<SecurityContext> newContext) {
super(SecurityContextHolder.class);
this.oldContext = oldContext;
this.newContext = newContext;
}
/** /**
* Construct an event * Construct an event
@ -36,9 +53,7 @@ public class SecurityContextChangedEvent extends ApplicationEvent {
* @param newContext the new security context * @param newContext the new security context
*/ */
public SecurityContextChangedEvent(SecurityContext oldContext, SecurityContext newContext) { public SecurityContextChangedEvent(SecurityContext oldContext, SecurityContext newContext) {
super(SecurityContextHolder.class); this(() -> oldContext, (newContext != null) ? () -> newContext : NO_CONTEXT);
this.oldContext = oldContext;
this.newContext = newContext;
} }
/** /**
@ -47,7 +62,7 @@ public class SecurityContextChangedEvent extends ApplicationEvent {
* @return the previous {@link SecurityContext} * @return the previous {@link SecurityContext}
*/ */
public SecurityContext getOldContext() { public SecurityContext getOldContext() {
return this.oldContext; return this.oldContext.get();
} }
/** /**
@ -56,7 +71,21 @@ public class SecurityContextChangedEvent extends ApplicationEvent {
* @return the current {@link SecurityContext} * @return the current {@link SecurityContext}
*/ */
public SecurityContext getNewContext() { public SecurityContext getNewContext() {
return this.newContext; return this.newContext.get();
}
/**
* Say whether the event is a context-clearing event.
*
* <p>
* This method is handy for avoiding looking up the new context to confirm it is a
* cleared event.
* @return {@code true} if the new context is
* {@link SecurityContextChangedEvent#NO_CONTEXT}
* @since 5.8
*/
public boolean isCleared() {
return this.newContext == NO_CONTEXT;
} }
} }

View File

@ -16,27 +16,36 @@
package org.springframework.security.core.context; package org.springframework.security.core.context;
import org.junit.jupiter.api.Test; import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class ListeningSecurityContextHolderStrategyTests { public class ListeningSecurityContextHolderStrategyTests {
@Test @Test
public void setContextWhenInvokedThenListenersAreNotified() { public void setContextWhenInvokedThenListenersAreNotified() {
SecurityContextHolderStrategy delegate = mock(SecurityContextHolderStrategy.class); SecurityContextHolderStrategy delegate = spy(new MockSecurityContextHolderStrategy());
SecurityContextChangedListener one = mock(SecurityContextChangedListener.class); SecurityContextChangedListener one = mock(SecurityContextChangedListener.class);
SecurityContextChangedListener two = mock(SecurityContextChangedListener.class); SecurityContextChangedListener two = mock(SecurityContextChangedListener.class);
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, one, two); SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, one, two);
given(delegate.createEmptyContext()).willReturn(new SecurityContextImpl()); given(delegate.createEmptyContext()).willReturn(new SecurityContextImpl());
SecurityContext context = strategy.createEmptyContext(); SecurityContext context = strategy.createEmptyContext();
strategy.setContext(context); strategy.setContext(context);
verify(delegate).setContext(context); strategy.getContext();
verify(one).securityContextChanged(any()); verify(one).securityContextChanged(any());
verify(two).securityContextChanged(any()); verify(two).securityContextChanged(any());
} }
@ -49,10 +58,68 @@ public class ListeningSecurityContextHolderStrategyTests {
SecurityContext context = new SecurityContextImpl(); SecurityContext context = new SecurityContextImpl();
given(delegate.getContext()).willReturn(context); given(delegate.getContext()).willReturn(context);
strategy.setContext(strategy.getContext()); strategy.setContext(strategy.getContext());
verify(delegate).setContext(context); strategy.getContext();
verifyNoInteractions(listener); verifyNoInteractions(listener);
} }
@Test
public void clearContextWhenNoGetContextThenContextIsNotRead() {
SecurityContextHolderStrategy delegate = mock(SecurityContextHolderStrategy.class);
SecurityContextChangedListener listener = mock(SecurityContextChangedListener.class);
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, listener);
Supplier<SecurityContext> context = mock(Supplier.class);
ArgumentCaptor<SecurityContextChangedEvent> event = ArgumentCaptor.forClass(SecurityContextChangedEvent.class);
given(delegate.getDeferredContext()).willReturn(context);
given(delegate.getContext()).willAnswer((invocation) -> context.get());
strategy.clearContext();
verifyNoInteractions(context);
verify(listener).securityContextChanged(event.capture());
assertThat(event.getValue().isCleared()).isTrue();
strategy.getContext();
verify(context).get();
strategy.clearContext();
verifyNoMoreInteractions(context);
}
@Test
public void getContextWhenCalledMultipleTimesThenEventPublishedOnce() {
SecurityContextHolderStrategy delegate = new MockSecurityContextHolderStrategy();
SecurityContextChangedListener listener = mock(SecurityContextChangedListener.class);
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, listener);
strategy.setContext(new SecurityContextImpl());
verifyNoInteractions(listener);
strategy.getContext();
verify(listener).securityContextChanged(any());
strategy.getContext();
verifyNoMoreInteractions(listener);
}
@Test
public void setContextWhenCalledMultipleTimesThenPublishedEventsAlign() {
SecurityContextHolderStrategy delegate = new MockSecurityContextHolderStrategy();
SecurityContextChangedListener listener = mock(SecurityContextChangedListener.class);
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, listener);
SecurityContext one = new SecurityContextImpl(new TestingAuthenticationToken("user", "pass"));
SecurityContext two = new SecurityContextImpl(new TestingAuthenticationToken("admin", "pass"));
ArgumentCaptor<SecurityContextChangedEvent> event = ArgumentCaptor.forClass(SecurityContextChangedEvent.class);
strategy.setContext(one);
strategy.setContext(two);
verifyNoInteractions(listener);
strategy.getContext();
verify(listener).securityContextChanged(event.capture());
assertThat(event.getValue().getOldContext()).isEqualTo(one);
assertThat(event.getValue().getNewContext()).isEqualTo(two);
strategy.getContext();
verifyNoMoreInteractions(listener);
strategy.setContext(one);
verifyNoMoreInteractions(listener);
reset(listener);
strategy.getContext();
verify(listener).securityContextChanged(event.capture());
assertThat(event.getValue().getOldContext()).isEqualTo(two);
assertThat(event.getValue().getNewContext()).isEqualTo(one);
}
@Test @Test
public void constructorWhenNullDelegateThenIllegalArgument() { public void constructorWhenNullDelegateThenIllegalArgument() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(

View File

@ -16,23 +16,35 @@
package org.springframework.security.core.context; package org.springframework.security.core.context;
import java.util.function.Supplier;
public class MockSecurityContextHolderStrategy implements SecurityContextHolderStrategy { public class MockSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private SecurityContext context; private Supplier<SecurityContext> context = () -> null;
@Override @Override
public void clearContext() { public void clearContext() {
this.context = null; this.context = () -> null;
} }
@Override @Override
public SecurityContext getContext() { public SecurityContext getContext() {
return this.context.get();
}
@Override
public Supplier<SecurityContext> getDeferredContext() {
return this.context; return this.context;
} }
@Override @Override
public void setContext(SecurityContext context) { public void setContext(SecurityContext context) {
this.context = context; this.context = () -> context;
}
@Override
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
this.context = deferredContext;
} }
@Override @Override