Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.core.context;

import java.util.function.Supplier;

import org.springframework.util.Assert;

/**
* A {@link Supplier} of an already-materialized {@link SecurityContext}. Marker class so
* {@link SecurityContextHolderThreadLocalAccessor} can inspect the value without
* triggering deferred materialization.
*/
final class ConstantSupplier implements Supplier<SecurityContext> {

private final SecurityContext context;

ConstantSupplier(SecurityContext context) {
Assert.notNull(context, "context cannot be null");
this.context = context;
}

@Override
public SecurityContext get() {
return this.context;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.security.core.context;

import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.springframework.util.Assert;
Expand Down Expand Up @@ -52,6 +54,12 @@ public void setContext(SecurityContext context) {
contextHolder = context;
}

@Override
public @Nullable Supplier<SecurityContext> peekDeferredContext() {
SecurityContext context = contextHolder;
return (context != null) ? new ConstantSupplier(context) : null;
}

@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.springframework.util.Assert;

/**
Expand Down Expand Up @@ -46,8 +48,7 @@ public SecurityContext getContext() {
public Supplier<SecurityContext> getDeferredContext() {
Supplier<SecurityContext> result = contextHolder.get();
if (result == null) {
SecurityContext context = createEmptyContext();
result = () -> context;
result = new ConstantSupplier(createEmptyContext());
contextHolder.set(result);
}
return result;
Expand All @@ -56,17 +57,15 @@ public Supplier<SecurityContext> getDeferredContext() {
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(() -> context);
contextHolder.set(new ConstantSupplier(context));
}

@Override
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
Assert.notNull(deferredContext, "Only non-null Supplier instances are permitted");
Supplier<SecurityContext> notNullDeferredContext = () -> {
SecurityContext result = deferredContext.get();
Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
return result;
};
Supplier<SecurityContext> notNullDeferredContext = (deferredContext instanceof NotNullSupplier
|| deferredContext instanceof ConstantSupplier) ? deferredContext
: new NotNullSupplier(deferredContext);
contextHolder.set(notNullDeferredContext);
}

Expand All @@ -75,4 +74,9 @@ public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}

@Override
public @Nullable Supplier<SecurityContext> peekDeferredContext() {
return contextHolder.get();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.springframework.util.Assert;

/**
Expand Down Expand Up @@ -150,6 +152,14 @@ public Supplier<SecurityContext> getDeferredContext() {
return this.delegate.getDeferredContext();
}

/**
* {@inheritDoc}
*/
@Override
public @Nullable Supplier<SecurityContext> peekDeferredContext() {
return this.delegate.peekDeferredContext();
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.core.context;

import java.util.function.Supplier;

import org.springframework.util.Assert;

/**
* Wraps a {@link Supplier} to reject {@code null} results. Marker class so
* {@link SecurityContextHolderStrategy} can skip re-wrapping on round-trip — without it,
* {@code setDeferredContext(getDeferredContext())} would accumulate one wrapper per call.
*/
final class NotNullSupplier implements Supplier<SecurityContext> {

private final Supplier<SecurityContext> delegate;

NotNullSupplier(Supplier<SecurityContext> delegate) {
Assert.notNull(delegate, "delegate cannot be null");
this.delegate = delegate;
}

@Override
public SecurityContext get() {
SecurityContext result = this.delegate.get();
Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
return result;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

/**
* A strategy for storing security context information against a thread.
*
Expand Down Expand Up @@ -51,6 +53,23 @@ default Supplier<SecurityContext> getDeferredContext() {
return this::getContext;
}

/**
* Obtains the current context as a {@link Supplier} if one is associated with the
* current thread, or {@code null} otherwise. Used by Micrometer Context Propagation
* during snapshot capture, so implementations SHOULD return the stored
* {@link Supplier} without invoking it and without auto-creating or storing a default
* context as a side effect.
* <p>
* The default implementation preserves the pre-7.1 propagation behavior: it
* materializes the current context eagerly and returns a {@link Supplier} of the
* captured context.
* @return the current context's {@link Supplier}, or {@code null} if none is set
* @since 7.1.1
*/
default @Nullable Supplier<SecurityContext> peekDeferredContext() {
return new ConstantSupplier(getContext());
}

/**
* Sets the current context.
* @param context to the new argument (should never be <code>null</code>, although
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.security.core.context;

import java.util.function.Supplier;

import io.micrometer.context.ThreadLocalAccessor;
import org.jspecify.annotations.Nullable;

Expand All @@ -30,30 +32,39 @@
* {@link SecurityContext} in Servlet applications. It is automatically registered with
* the {@link io.micrometer.context.ContextRegistry} through the
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath.
* <p>
* The propagated value is the deferred {@link Supplier}, so capture does not materialize
* the {@link SecurityContext}; the supplier is invoked lazily on the consuming thread. An
* already-materialized {@link SecurityContext} that equals the empty context is treated
* as absent and is not propagated.
*
* @author Steve Riesenberg
* @since 6.5
* @see io.micrometer.context.ContextRegistry
*/
public final class SecurityContextHolderThreadLocalAccessor implements ThreadLocalAccessor<SecurityContext> {
public final class SecurityContextHolderThreadLocalAccessor implements ThreadLocalAccessor<Supplier<SecurityContext>> {

@Override
public Object key() {
return SecurityContext.class.getName();
}

@Override
public @Nullable SecurityContext getValue() {
SecurityContext securityContext = SecurityContextHolder.getContext();
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();

return !securityContext.equals(emptyContext) ? securityContext : null;
public @Nullable Supplier<SecurityContext> getValue() {
Supplier<SecurityContext> deferred = SecurityContextHolder.getContextHolderStrategy().peekDeferredContext();
// The empty check is only possible when the context is already materialized;
// invoking a deferred supplier here would reintroduce gh-18059.
if (deferred instanceof ConstantSupplier constant
&& constant.get().equals(SecurityContextHolder.createEmptyContext())) {
return null;
}
return deferred;
}

@Override
public void setValue(SecurityContext securityContext) {
public void setValue(Supplier<SecurityContext> securityContext) {
Assert.notNull(securityContext, "securityContext cannot be null");
SecurityContextHolder.setContext(securityContext);
SecurityContextHolder.getContextHolderStrategy().setDeferredContext(securityContext);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.springframework.util.Assert;

/**
Expand Down Expand Up @@ -47,8 +49,7 @@ public SecurityContext getContext() {
public Supplier<SecurityContext> getDeferredContext() {
Supplier<SecurityContext> result = contextHolder.get();
if (result == null) {
SecurityContext context = createEmptyContext();
result = () -> context;
result = new ConstantSupplier(createEmptyContext());
contextHolder.set(result);
}
return result;
Expand All @@ -57,17 +58,15 @@ public Supplier<SecurityContext> getDeferredContext() {
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(() -> context);
contextHolder.set(new ConstantSupplier(context));
}

@Override
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
Assert.notNull(deferredContext, "Only non-null Supplier instances are permitted");
Supplier<SecurityContext> notNullDeferredContext = () -> {
SecurityContext result = deferredContext.get();
Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
return result;
};
Supplier<SecurityContext> notNullDeferredContext = (deferredContext instanceof NotNullSupplier
|| deferredContext instanceof ConstantSupplier) ? deferredContext
: new NotNullSupplier(deferredContext);
contextHolder.set(notNullDeferredContext);
}

Expand All @@ -76,4 +75,9 @@ public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}

@Override
public @Nullable Supplier<SecurityContext> peekDeferredContext() {
return contextHolder.get();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.core.context;

import java.util.function.Supplier;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import org.springframework.security.core.Authentication;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

class GlobalSecurityContextHolderStrategyTests {

GlobalSecurityContextHolderStrategy strategy = new GlobalSecurityContextHolderStrategy();

@AfterEach
void clearContext() {
this.strategy.clearContext();
}

// gh-18059
@Test
void peekDeferredContextWhenEmptyThenReturnsNull() {
assertThat(this.strategy.peekDeferredContext()).isNull();
}

// gh-18059
@Test
void peekDeferredContextWhenSetThenReturnsSupplierOfSameContext() {
Authentication authentication = mock(Authentication.class);
SecurityContext context = new SecurityContextImpl(authentication);
this.strategy.setContext(context);
Supplier<SecurityContext> deferred = this.strategy.peekDeferredContext();
assertThat(deferred).isNotNull();
assertThat(deferred.get()).isSameAs(context);
}

// gh-18059
@Test
void peekDeferredContextWhenContextAutoCreatedThenReturnsSupplierOfSameContext() {
SecurityContext context = this.strategy.getContext();
Supplier<SecurityContext> deferred = this.strategy.peekDeferredContext();
assertThat(deferred).isNotNull();
assertThat(deferred.get()).isSameAs(context);
}

}
Loading