From c60ef02cbfd567f3811d7422322b52e4f83f69dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 7 Nov 2024 17:26:53 +0100 Subject: [PATCH] Add TBV test including a bytebuddy generated proxy object --- .../src/test/java/foo/bar/Author.java | 62 ++++++++++ .../src/test/java/foo/bar/Book.java | 90 ++++++++++++++ .../src/test/java/foo/bar/Library.java | 76 ++++++++++++ .../src/test/java/foo/bar/Owner.java | 62 ++++++++++ dd-smoke-tests/springboot-jpa/build.gradle | 30 +++++ .../springboot/SpringbootApplication.java | 20 +++ .../controller/LibraryController.java | 114 ++++++++++++++++++ .../smoketest/springboot/entity/Author.java | 31 +++++ .../smoketest/springboot/entity/Book.java | 40 ++++++ .../smoketest/springboot/entity/Library.java | 50 ++++++++ .../smoketest/springboot/entity/Owner.java | 34 ++++++ .../filter/SessionVisitorFilter.java | 113 +++++++++++++++++ .../springboot/service/LibraryService.java | 43 +++++++ .../src/main/resources/application.yml | 7 ++ .../src/main/webapp/WEB-INF/jsp/update.jsp | 59 +++++++++ .../SpringBootIastJpaIntegrationTest.groovy | 81 +++++++++++++ settings.gradle | 1 + 17 files changed, 913 insertions(+) create mode 100644 dd-java-agent/agent-iast/src/test/java/foo/bar/Author.java create mode 100644 dd-java-agent/agent-iast/src/test/java/foo/bar/Book.java create mode 100644 dd-java-agent/agent-iast/src/test/java/foo/bar/Library.java create mode 100644 dd-java-agent/agent-iast/src/test/java/foo/bar/Owner.java create mode 100644 dd-smoke-tests/springboot-jpa/build.gradle create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/controller/LibraryController.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Author.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Book.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Library.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Owner.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/filter/SessionVisitorFilter.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/service/LibraryService.java create mode 100644 dd-smoke-tests/springboot-jpa/src/main/resources/application.yml create mode 100644 dd-smoke-tests/springboot-jpa/src/main/webapp/WEB-INF/jsp/update.jsp create mode 100644 dd-smoke-tests/springboot-jpa/src/test/groovy/datadog/smoketest/SpringBootIastJpaIntegrationTest.groovy diff --git a/dd-java-agent/agent-iast/src/test/java/foo/bar/Author.java b/dd-java-agent/agent-iast/src/test/java/foo/bar/Author.java new file mode 100644 index 00000000000..cfed685dd44 --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/java/foo/bar/Author.java @@ -0,0 +1,62 @@ +package foo.bar; + +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Author { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @Column(name = "name") + private String name; + + private int updateCount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getUpdateCount() { + return updateCount; + } + + public void setUpdateCount(int updateCount) { + this.updateCount = updateCount; + } + + public void increaseUpdateCount() { + this.updateCount++; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Author author = (Author) o; + return Objects.equals(id, author.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/dd-java-agent/agent-iast/src/test/java/foo/bar/Book.java b/dd-java-agent/agent-iast/src/test/java/foo/bar/Book.java new file mode 100644 index 00000000000..3cdeda8e279 --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/java/foo/bar/Book.java @@ -0,0 +1,90 @@ +package foo.bar; + +import java.util.List; +import java.util.Objects; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; + +@Entity +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "book_id") + private List authors; + + @OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id") + private Owner owner; + + private int updateCount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + public Owner getOwner() { + return owner; + } + + public void setOwner(Owner owner) { + this.owner = owner; + } + + public int getUpdateCount() { + return updateCount; + } + + public void setUpdateCount(int updateCount) { + this.updateCount = updateCount; + } + + public void increaseUpdateCount() { + this.updateCount++; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Book book = (Book) o; + return Objects.equals(id, book.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/dd-java-agent/agent-iast/src/test/java/foo/bar/Library.java b/dd-java-agent/agent-iast/src/test/java/foo/bar/Library.java new file mode 100644 index 00000000000..015e01da0a7 --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/java/foo/bar/Library.java @@ -0,0 +1,76 @@ +package foo.bar; + +import java.util.List; +import java.util.Objects; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; + +@Entity +public class Library { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "library_id") + private List books; + + private int updateCount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getBooks() { + return books; + } + + public void setBooks(List books) { + this.books = books; + } + + public int getUpdateCount() { + return updateCount; + } + + public void setUpdateCount(int updateCount) { + this.updateCount = updateCount; + } + + public boolean isIssueExists() { + for (Book book : this.getBooks()) { + if (book.getUpdateCount() != book.getOwner().getUpdateCount()) { + return true; + } + for (Author author : book.getAuthors()) { + if (book.getUpdateCount() != author.getUpdateCount()) { + return true; + } + } + } + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Library library = (Library) o; + return Objects.equals(id, library.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/dd-java-agent/agent-iast/src/test/java/foo/bar/Owner.java b/dd-java-agent/agent-iast/src/test/java/foo/bar/Owner.java new file mode 100644 index 00000000000..66ac58476dc --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/java/foo/bar/Owner.java @@ -0,0 +1,62 @@ +package foo.bar; + +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Owner { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @Column(name = "name") + private String name; + + private int updateCount; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getUpdateCount() { + return updateCount; + } + + public void setUpdateCount(int updateCount) { + this.updateCount = updateCount; + } + + public void increaseUpdateCount() { + this.updateCount++; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Owner owner = (Owner) o; + return Objects.equals(id, owner.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/dd-smoke-tests/springboot-jpa/build.gradle b/dd-smoke-tests/springboot-jpa/build.gradle new file mode 100644 index 00000000000..05e9c8212db --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'war' + id 'org.springframework.boot' version '2.6.0' +} + +apply plugin: 'io.spring.dependency-management' +apply from: "$rootDir/gradle/java.gradle" +description = 'SpringBoot JPA Smoke Tests.' + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' + implementation 'javax.servlet:jstl:1.2' + implementation 'com.h2database:h2:2.1.214' + + testImplementation project(':dd-smoke-tests') + + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' +} + +tasks.withType(Test).configureEach { + dependsOn "bootWar" + jvmArgs "-Ddatadog.smoketest.springboot.bootWar.path=${tasks.bootWar.archiveFile.get()}" +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java new file mode 100644 index 00000000000..7c858384413 --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java @@ -0,0 +1,20 @@ +package datadog.smoketest.springboot; + +import datadog.smoketest.springboot.filter.SessionVisitorFilter; +import javax.servlet.Filter; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SpringbootApplication { + + @Bean + public Filter badBehaviorFilter() { + return new SessionVisitorFilter(); + } + + public static void main(final String[] args) { + SpringApplication.run(SpringbootApplication.class, args); + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/controller/LibraryController.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/controller/LibraryController.java new file mode 100644 index 00000000000..e8a6891ff32 --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/controller/LibraryController.java @@ -0,0 +1,114 @@ +package datadog.smoketest.springboot.controller; + +import static java.util.Arrays.asList; + +import datadog.smoketest.springboot.entity.Author; +import datadog.smoketest.springboot.entity.Book; +import datadog.smoketest.springboot.entity.Library; +import datadog.smoketest.springboot.entity.Owner; +import datadog.smoketest.springboot.service.LibraryService; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.hibernate.collection.internal.PersistentBag; +import org.hibernate.proxy.HibernateProxy; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.bind.support.SessionStatus; + +@Controller +@RequestMapping("/library") +@SessionAttributes(LibraryController.COMMAND_NAME) +@RequiredArgsConstructor +public class LibraryController { + + public static final String COMMAND_NAME = "library"; + + private final LibraryService libraryService; + + @GetMapping("/{id}") + public ResponseEntity hasIssue(@PathVariable final int id) { + return ResponseEntity.ok(libraryService.findLibraryById(id).isIssueExists()); + } + + @GetMapping + public ResponseEntity create() { + final Library library = + Library.builder() + .books( + asList( + Book.builder() + .title("The Lord of the Rings") + .owner(Owner.builder().name("Peter Jackson").build()) + .authors( + asList( + Author.builder().name("J.R.R Tolkien").build(), + Author.builder().name("Peter Jackson").build())) + .build(), + Book.builder() + .title("The Hobbit") + .owner(Owner.builder().name("Edith Tolkien").build()) + .authors( + asList( + Author.builder().name("J.R.R Tolkien").build(), + Author.builder().name("Edith Tolkien").build())) + .build())) + .build(); + libraryService.save(library); + return ResponseEntity.ok(library.getId()); + } + + @GetMapping("/update/{id}") + public String update(@PathVariable final int id, final ModelMap model) { + final Library library = libraryService.findLibraryById(id); + model.put(COMMAND_NAME, library); + return "update"; + } + + @GetMapping("/update") + public String update( + @ModelAttribute(COMMAND_NAME) final Library library, final SessionStatus sessionStatus) { + libraryService.update(library); + sessionStatus.setComplete(); + return "redirect:/library/" + library.getId(); + } + + @GetMapping("/session/add/{id}") + public ResponseEntity addToSession( + @RequestParam("mode") final String mode, + @PathVariable final int id, + final HttpServletRequest request) { + final Library library = libraryService.findLibraryById(id); + final Book book = library.getBooks().get(0); + final HttpSession session = request.getSession(); + if (mode.equals("one-to-one")) { + session.setAttribute(mode, book.getOwner()); + } else { + session.setAttribute(mode, book.getAuthors()); + } + return ResponseEntity.ok("OK"); + } + + @GetMapping("/session/validate") + public ResponseEntity validateSession( + @RequestParam("mode") final String mode, final HttpServletRequest request) { + final HttpSession session = request.getSession(); + final Object sessionItem = session.getAttribute(mode); + final boolean loaded; + if (sessionItem instanceof Owner) { + final HibernateProxy proxy = (HibernateProxy) sessionItem; + loaded = !proxy.getHibernateLazyInitializer().isUninitialized(); + } else { + final PersistentBag bag = (PersistentBag) sessionItem; + loaded = bag.wasInitialized(); + } + return ResponseEntity.ok(loaded); + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Author.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Author.java new file mode 100644 index 00000000000..d852626cab3 --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Author.java @@ -0,0 +1,31 @@ +package datadog.smoketest.springboot.entity; + +import javax.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode(of = "id") +public class Author { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private int id; + + private String name; + + private int updateCount; + + public void increaseUpdateCount() { + this.updateCount++; + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Book.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Book.java new file mode 100644 index 00000000000..93f97d6805c --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Book.java @@ -0,0 +1,40 @@ +package datadog.smoketest.springboot.entity; + +import java.util.List; +import javax.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode(of = "id") +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private int id; + + private String title; + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "book_id") + private List authors; + + @OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY) + @JoinColumn(name = "owner_id") + private Owner owner; + + private int updateCount; + + public void increaseUpdateCount() { + this.updateCount++; + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Library.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Library.java new file mode 100644 index 00000000000..3742168318d --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Library.java @@ -0,0 +1,50 @@ +package datadog.smoketest.springboot.entity; + +import java.util.List; +import javax.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode(of = "id") +public class Library { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private int id; + + private String name; + + @OneToMany(cascade = CascadeType.ALL) + @JoinColumn(name = "library_id") + private List books; + + private int updateCount; + + public void increaseUpdateCount() { + this.updateCount++; + } + + public boolean isIssueExists() { + for (Book book : this.getBooks()) { + if (book.getUpdateCount() != book.getOwner().getUpdateCount()) { + return true; + } + for (Author author : book.getAuthors()) { + if (book.getUpdateCount() != author.getUpdateCount()) { + return true; + } + } + } + return false; + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Owner.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Owner.java new file mode 100644 index 00000000000..5273fe7c76d --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/entity/Owner.java @@ -0,0 +1,34 @@ +package datadog.smoketest.springboot.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode(of = "id") +public class Owner { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private int id; + + private String name; + + private int updateCount; + + public void increaseUpdateCount() { + this.updateCount++; + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/filter/SessionVisitorFilter.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/filter/SessionVisitorFilter.java new file mode 100644 index 00000000000..57186f599fa --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/filter/SessionVisitorFilter.java @@ -0,0 +1,113 @@ +package datadog.smoketest.springboot.filter; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import lombok.experimental.Delegate; +import org.springframework.web.filter.OncePerRequestFilter; + +public class SessionVisitorFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) + throws ServletException, IOException { + request = hasSessionHeader(request) ? new RequestWrapper(request) : request; + filterChain.doFilter(request, response); + } + + private static boolean hasSessionHeader(final HttpServletRequest request) { + return request.getHeader("X-Session-Visitor") != null; + } + + private static class RequestWrapper extends HttpServletRequestWrapper { + public RequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public HttpSession getSession(boolean create) { + return wrapSession(super.getSession(create)); + } + + @Override + public HttpSession getSession() { + return wrapSession(super.getSession()); + } + + private HttpSession wrapSession(final HttpSession session) { + if (session == null || session instanceof SessionWrapper) { + return session; + } + return new SessionWrapper(session); + } + } + + private static class SessionWrapper implements HttpSession { + + @Delegate private final HttpSession delegate; + + private SessionWrapper(final HttpSession delegate) { + this.delegate = delegate; + } + + public void setAttribute(final String name, final Object value) { + new DumbVisitor().visit(value); + delegate.setAttribute(name, value); + } + } + + /** + * Extremely unsafe visitor class used to trigger some bad behaviour with Hibernate and lazy + * properties + */ + private static class DumbVisitor { + private final Set visited = new HashSet<>(); + + public void visit(final Object value) { + if (value == null || visited.contains(value)) { + return; + } + visited.add(value); + if (value.getClass().isArray()) { + for (Object item : (Object[]) value) { + visitObject(item); + } + } else if (value instanceof Iterable) { + for (Object item : (Iterable) value) { + visitObject(item); + } + } else if (value instanceof Map) { + for (Object item : ((Map) value).values()) { + visitObject(item); + } + } else { + visitObject(value); + } + } + + private void visitObject(final Object object) { + Class klass = object.getClass(); + while (klass != Object.class) { + for (final Field field : klass.getDeclaredFields()) { + try { + field.setAccessible(true); + final Object value = field.get(object); + visit(value); + } catch (final Throwable e) { + // ignore it + } + } + klass = klass.getSuperclass(); + } + } + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/service/LibraryService.java b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/service/LibraryService.java new file mode 100644 index 00000000000..1f211db1753 --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/java/datadog/smoketest/springboot/service/LibraryService.java @@ -0,0 +1,43 @@ +package datadog.smoketest.springboot.service; + +import datadog.smoketest.springboot.entity.Author; +import datadog.smoketest.springboot.entity.Book; +import datadog.smoketest.springboot.entity.Library; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; +import org.springframework.stereotype.Service; + +@Service +public class LibraryService { + + @PersistenceContext private EntityManager em; + + @Transactional + public void save(final Library library) { + em.persist(library); + } + + @Transactional + public Library update(Library library) { + library.increaseUpdateCount(); + library.getBooks().forEach(Book::increaseUpdateCount); + library.getBooks().stream() + .map(Book::getId) + .map(this::findBookById) + .forEach( + book -> { + book.getAuthors().forEach(Author::increaseUpdateCount); + book.getOwner().increaseUpdateCount(); + }); + return em.merge(library); + } + + public Library findLibraryById(int id) { + return em.find(Library.class, id); + } + + public Book findBookById(int id) { + return em.find(Book.class, id); + } +} diff --git a/dd-smoke-tests/springboot-jpa/src/main/resources/application.yml b/dd-smoke-tests/springboot-jpa/src/main/resources/application.yml new file mode 100644 index 00000000000..7be64e1c1f3 --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + mvc: + view: + prefix: /WEB-INF/jsp/ + suffix: .jsp + jpa: + show-sql: true diff --git a/dd-smoke-tests/springboot-jpa/src/main/webapp/WEB-INF/jsp/update.jsp b/dd-smoke-tests/springboot-jpa/src/main/webapp/WEB-INF/jsp/update.jsp new file mode 100644 index 00000000000..c4e32ffefea --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/main/webapp/WEB-INF/jsp/update.jsp @@ -0,0 +1,59 @@ +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%-- + User: shahriarmohaiminul + Date: 9/8/24 + Time: 1:28 PM +--%> +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + + Library Update + + + +
+
+

Update Library

+ + + +

+ Library ID: + +

+ +

+ Update Count: + +

+ + + + + + + + + + + + + + + + + + + +
IDTitleUpdate Count
+ +
+ +
+
+
+
+ + diff --git a/dd-smoke-tests/springboot-jpa/src/test/groovy/datadog/smoketest/SpringBootIastJpaIntegrationTest.groovy b/dd-smoke-tests/springboot-jpa/src/test/groovy/datadog/smoketest/SpringBootIastJpaIntegrationTest.groovy new file mode 100644 index 00000000000..1ee7f6f81de --- /dev/null +++ b/dd-smoke-tests/springboot-jpa/src/test/groovy/datadog/smoketest/SpringBootIastJpaIntegrationTest.groovy @@ -0,0 +1,81 @@ +package datadog.smoketest + + +import okhttp3.Request + +import static datadog.trace.agent.test.utils.OkHttpUtils.clientBuilder +import static datadog.trace.agent.test.utils.OkHttpUtils.cookieJar + +class SpringBootIastJpaIntegrationTest extends AbstractServerSmokeTest { + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.bootWar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll((String[]) [ + "-Ddd.iast.enabled=true", + "-jar", + springBootShadowJar, + "--server.port=${httpPort}" + ]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + void 'test customer issue with one-to-many relation, add visitor: #forceVisitor'() { + setup: + + final client = clientBuilder().cookieJar(cookieJar()).build() + final createLib1 = client.newCall(request(forceVisitor).build()).execute() + final createLib2 = client.newCall(request(forceVisitor).build()).execute() + assert ![createLib1, createLib2].any { it.code() != 200 } + final libraryId = createLib1.body().string().toInteger() + + when: + // trigger + final updateForm = client.newCall(request(forceVisitor, "/update/$libraryId").build()).execute() + final libraryResult = client.newCall(request(forceVisitor, "/update").build()).execute() + assert ![updateForm, libraryResult].any { it.code() != 200 } + + then: + final hasIssue = libraryResult.body().string().toBoolean() + hasIssue == forceVisitor + + where: + forceVisitor << [false, true] + } + + void 'validate that IAST does not trigger lazy relations, mode: #mode'() { + setup: + final client = clientBuilder().cookieJar(cookieJar()).build() + final createLib = client.newCall(request().build()).execute() + assert createLib.code() == 200 + final libraryId = createLib.body().string().toInteger() + + when: + final test = client.newCall(request("/session/add/$libraryId?mode=$mode").build()).execute() + final validate = client.newCall(request("/session/validate?mode=$mode").build()).execute() + + then: + assert ![test, validate].any { it.code() != 200 } + assert validate.body().string().toBoolean() == false : "Relation should not be triggered by IAST" + + where: + mode << ['one-to-one', 'one-to-many'] + } + + private Request.Builder request(String suffix = '') { + return new Request.Builder().url("http://localhost:${httpPort}/library$suffix").get() + } + + private Request.Builder request(boolean forceVisitor, String suffix = '') { + def builder = request(suffix) + if (forceVisitor) { + builder = builder.addHeader('X-Session-Visitor', 'true') + } + return builder + } +} diff --git a/settings.gradle b/settings.gradle index 6e6bb98a524..07a9bc3131d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -140,6 +140,7 @@ include ':dd-smoke-tests:springboot' include ':dd-smoke-tests:springboot-freemarker' include ':dd-smoke-tests:springboot-grpc' include ':dd-smoke-tests:springboot-jetty-jsp' +include ':dd-smoke-tests:springboot-jpa' include ':dd-smoke-tests:springboot-mongo' include ':dd-smoke-tests:springboot-openliberty-20' include ':dd-smoke-tests:springboot-openliberty-23'