diff --git a/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java b/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java index d7285e7f976..7bfb63e06a1 100644 --- a/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java +++ b/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/SpringSecurityUserEventDecorator.java @@ -43,6 +43,10 @@ public void onUserNotFound() { tracker.onUserNotFound(UserIdCollectionMode.get()); } + public void onUserNotFound(final String username) { + onUserNotFound(); + } + public void onSignup(UserDetails user, Throwable throwable) { // skip failures while signing up a user, later on, we might want to generate a separate event // for this case diff --git a/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/UsernameNotFoundExceptionFactoryInstrumentation.java b/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/UsernameNotFoundExceptionFactoryInstrumentation.java new file mode 100644 index 00000000000..e531fef6b25 --- /dev/null +++ b/dd-java-agent/instrumentation/spring/spring-security/spring-security-5.0/src/main/java/datadog/trace/instrumentation/springsecurity5/UsernameNotFoundExceptionFactoryInstrumentation.java @@ -0,0 +1,54 @@ +package datadog.trace.instrumentation.springsecurity5; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import net.bytebuddy.asm.Advice; + +/** + * Hooks the static factory method {@code UsernameNotFoundException.fromUsername(String)} added in + * Spring Security 7. In Spring Security 7, {@code UserDetailsService} implementations (notably + * {@code InMemoryUserDetailsManager}) construct {@code UsernameNotFoundException} via this factory + * rather than the public constructor, so the sibling {@link UsernameNotFoundExceptionInstrumentation} + * (constructor-based) no longer fires on that path. + */ +@AutoService(InstrumenterModule.class) +public class UsernameNotFoundExceptionFactoryInstrumentation extends InstrumenterModule.AppSec + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public UsernameNotFoundExceptionFactoryInstrumentation() { + super("spring-security"); + } + + @Override + public String instrumentedType() { + return "org.springframework.security.core.userdetails.UsernameNotFoundException"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.instrumentation.springsecurity5.SpringSecurityUserEventDecorator" + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("fromUsername")).and(isStatic()).and(isPublic()), + getClass().getName() + "$FromUsernameAdvice"); + } + + public static class FromUsernameAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) final String username) { + SpringSecurityUserEventDecorator.DECORATE.onUserNotFound(username); + } + } +} diff --git a/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/build.gradle b/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/build.gradle index e271c5ad55c..83968e41118 100644 --- a/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/build.gradle +++ b/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/build.gradle @@ -24,9 +24,9 @@ dependencies { testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: springBootVersion - latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.+' - latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '3.+' - latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.+' + latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '4.+' + latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '4.+' + latestDepTestImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '4.+' testRuntimeOnly project(':dd-java-agent:instrumentation:tomcat:tomcat-appsec:tomcat-appsec-6.0') testRuntimeOnly project(':dd-java-agent:instrumentation:tomcat:tomcat-5.5') diff --git a/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/src/test/java/datadog/trace/instrumentation/springsecurity6/UsernameNotFoundFromUsernameTest.java b/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/src/test/java/datadog/trace/instrumentation/springsecurity6/UsernameNotFoundFromUsernameTest.java new file mode 100644 index 00000000000..d2e949c0182 --- /dev/null +++ b/dd-java-agent/instrumentation/spring/spring-security/spring-security-6.0/src/test/java/datadog/trace/instrumentation/springsecurity6/UsernameNotFoundFromUsernameTest.java @@ -0,0 +1,40 @@ +package datadog.trace.instrumentation.springsecurity6; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.instrumentation.springsecurity5.SpringSecurityUserEventDecorator; +import datadog.trace.instrumentation.springsecurity5.UsernameNotFoundExceptionFactoryInstrumentation; +import org.junit.jupiter.api.Test; + +/** + * Unit-level checks for the Spring Security 7 {@code UsernameNotFoundException.fromUsername} + * factory hook. + * + *
The end-to-end AppSec event assertion lives in the existing {@code SpringBootBasedTest} + * Groovy spec; when {@code latestDepTest} resolves Spring Boot 4 (Spring Security 7) the + * "test failed login with non existing user" case exercises the {@code fromUsername} factory + * path through {@code InMemoryUserDetailsManager} and verifies the AppSec login-failure tags + * are emitted. This Java test pins the contract pieces that aren't covered there: the new + * instrumentation targets the exact {@code UsernameNotFoundException} type, and the decorator + * overload that the advice invokes is callable without throwing. + */ +class UsernameNotFoundFromUsernameTest { + + @Test + void instrumentationTargetsUsernameNotFoundException() { + final UsernameNotFoundExceptionFactoryInstrumentation instrumentation = + new UsernameNotFoundExceptionFactoryInstrumentation(); + assertEquals( + "org.springframework.security.core.userdetails.UsernameNotFoundException", + instrumentation.instrumentedType()); + } + + @Test + void decoratorAcceptsUsernameOverload() { + // The advice forwards Argument(0) into this overload; ensure it is callable in isolation + // (no AppSec tracker is registered in this unit test, so the decorator must no-op cleanly). + assertDoesNotThrow(() -> SpringSecurityUserEventDecorator.DECORATE.onUserNotFound("alice")); + assertDoesNotThrow(() -> SpringSecurityUserEventDecorator.DECORATE.onUserNotFound(null)); + } +}