Skip to content
Closed
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
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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));
}
}
Loading