Fix report card (#1845)

* fix: report card

Fix and simplify calculation of the number of assignments a user solved.
Rename `UserTracker` to `UserProgress`
Rename `LessonTracker` to `LessonProgress`
Rename tables in database
This commit is contained in:
Nanne Baars
2024-07-09 20:07:09 +02:00
committed by GitHub
parent 1531987da5
commit a0b6decf34
27 changed files with 237 additions and 248 deletions

View File

@ -23,8 +23,8 @@
package org.owasp.webgoat.container.assignments;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.UserTracker;
import org.owasp.webgoat.container.users.UserTrackerRepository;
import org.owasp.webgoat.container.users.UserProgress;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@ -36,11 +36,11 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class LessonTrackerInterceptor implements ResponseBodyAdvice<Object> {
private UserTrackerRepository userTrackerRepository;
private UserProgressRepository userTrackerRepository;
private WebSession webSession;
public LessonTrackerInterceptor(
UserTrackerRepository userTrackerRepository, WebSession webSession) {
UserProgressRepository userTrackerRepository, WebSession webSession) {
this.userTrackerRepository = userTrackerRepository;
this.webSession = webSession;
}
@ -66,9 +66,9 @@ public class LessonTrackerInterceptor implements ResponseBodyAdvice<Object> {
}
protected AttackResult trackProgress(AttackResult attackResult) {
UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName());
UserProgress userTracker = userTrackerRepository.findByUser(webSession.getUserName());
if (userTracker == null) {
userTracker = new UserTracker(webSession.getUserName());
userTracker = new UserProgress(webSession.getUserName());
}
if (attackResult.assignmentSolved()) {
userTracker.assignmentSolved(webSession.getCurrentLesson(), attackResult.getAssignment());

View File

@ -54,7 +54,7 @@ public class Assignment {
@Transient private List<String> hints;
private Assignment() {
protected Assignment() {
// Hibernate
}

View File

@ -0,0 +1,3 @@
package org.owasp.webgoat.container.report;
record LessonStatistics(String name, boolean solved, int numberOfAttempts) {}

View File

@ -0,0 +1,93 @@
/**
* *************************************************************************************************
*
* <p>
*
* <p>This file is part of WebGoat, an Open Web Application Security Project utility. For details,
* please see http://www.owasp.org/
*
* <p>Copyright (c) 2002 - 2014 Bruce Mayhew
*
* <p>This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* <p>You should have received a copy of the GNU General Public License along with this program; if
* not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*
* <p>Getting Source ==============
*
* <p>Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository
* for free software projects.
*/
package org.owasp.webgoat.container.report;
import java.util.List;
import org.owasp.webgoat.container.i18n.PluginMessages;
import org.owasp.webgoat.container.session.Course;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ReportCardController {
private final WebSession webSession;
private final UserProgressRepository userProgressRepository;
private final Course course;
private final PluginMessages pluginMessages;
public ReportCardController(
WebSession webSession,
UserProgressRepository userProgressRepository,
Course course,
PluginMessages pluginMessages) {
this.webSession = webSession;
this.userProgressRepository = userProgressRepository;
this.course = course;
this.pluginMessages = pluginMessages;
}
/**
* Endpoint which generates the report card for the current use to show the stats on the solved
* lessons
*/
@GetMapping(path = "/service/reportcard.mvc", produces = "application/json")
@ResponseBody
public ReportCard reportCard() {
var userProgress = userProgressRepository.findByUser(webSession.getUserName());
var lessonStatistics =
course.getLessons().stream()
.map(
lesson -> {
var lessonTracker = userProgress.getLessonProgress(lesson);
return new LessonStatistics(
pluginMessages.getMessage(lesson.getTitle()),
lessonTracker.isLessonSolved(),
lessonTracker.getNumberOfAttempts());
})
.toList();
return new ReportCard(
course.getTotalOfLessons(),
course.getTotalOfAssignments(),
userProgress.numberOfAssignmentsSolved(),
userProgress.numberOfLessonsSolved(),
lessonStatistics);
}
private record ReportCard(
int totalNumberOfLessons,
int totalNumberOfAssignments,
long numberOfAssignmentsSolved,
long numberOfLessonsSolved,
List<LessonStatistics> lessonStatistics) {}
private record LessonStatistics(String name, boolean solved, int numberOfAttempts) {}
}

View File

@ -39,9 +39,9 @@ import org.owasp.webgoat.container.lessons.LessonMenuItem;
import org.owasp.webgoat.container.lessons.LessonMenuItemType;
import org.owasp.webgoat.container.session.Course;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.LessonTracker;
import org.owasp.webgoat.container.users.UserTracker;
import org.owasp.webgoat.container.users.UserTrackerRepository;
import org.owasp.webgoat.container.users.LessonProgress;
import org.owasp.webgoat.container.users.UserProgress;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@ -60,7 +60,7 @@ public class LessonMenuService {
public static final String URL_LESSONMENU_MVC = "/service/lessonmenu.mvc";
private final Course course;
private final WebSession webSession;
private UserTrackerRepository userTrackerRepository;
private UserProgressRepository userTrackerRepository;
@Value("#{'${exclude.categories}'.split(',')}")
private List<String> excludeCategories;
@ -77,7 +77,7 @@ public class LessonMenuService {
public @ResponseBody List<LessonMenuItem> showLeftNav() {
List<LessonMenuItem> menu = new ArrayList<>();
List<Category> categories = course.getCategories();
UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName());
UserProgress userTracker = userTrackerRepository.findByUser(webSession.getUserName());
for (Category category : categories) {
if (excludeCategories.contains(category.name())) {
@ -97,7 +97,7 @@ public class LessonMenuService {
lessonItem.setName(lesson.getTitle());
lessonItem.setLink(lesson.getLink());
lessonItem.setType(LessonMenuItemType.LESSON);
LessonTracker lessonTracker = userTracker.getLessonTracker(lesson);
LessonProgress lessonTracker = userTracker.getLessonProgress(lesson);
boolean lessonSolved = lessonCompleted(lessonTracker.getLessonOverview(), lesson);
lessonItem.setComplete(lessonSolved);
categoryItem.addChild(lessonItem);

View File

@ -6,7 +6,7 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.owasp.webgoat.container.lessons.Assignment;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.UserTrackerRepository;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
@RequiredArgsConstructor
public class LessonProgressService {
private final UserTrackerRepository userTrackerRepository;
private final UserProgressRepository userTrackerRepository;
private final WebSession webSession;
/**
@ -36,7 +36,7 @@ public class LessonProgressService {
var currentLesson = webSession.getCurrentLesson();
if (currentLesson != null) {
var lessonTracker = userTracker.getLessonTracker(currentLesson);
var lessonTracker = userTracker.getLessonProgress(currentLesson);
return lessonTracker.getLessonOverview().entrySet().stream()
.map(entry -> new LessonOverview(entry.getKey(), entry.getValue()))
.toList();

View File

@ -1,105 +0,0 @@
/**
* *************************************************************************************************
*
* <p>
*
* <p>This file is part of WebGoat, an Open Web Application Security Project utility. For details,
* please see http://www.owasp.org/
*
* <p>Copyright (c) 2002 - 2014 Bruce Mayhew
*
* <p>This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* <p>You should have received a copy of the GNU General Public License along with this program; if
* not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*
* <p>Getting Source ==============
*
* <p>Source for this application is maintained at https://github.com/WebGoat/WebGoat, a repository
* for free software projects.
*/
package org.owasp.webgoat.container.service;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.owasp.webgoat.container.i18n.PluginMessages;
import org.owasp.webgoat.container.lessons.Lesson;
import org.owasp.webgoat.container.session.Course;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.LessonTracker;
import org.owasp.webgoat.container.users.UserTracker;
import org.owasp.webgoat.container.users.UserTrackerRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* ReportCardService
*
* @author nbaars
* @version $Id: $Id
*/
@Controller
@AllArgsConstructor
public class ReportCardService {
private final WebSession webSession;
private final UserTrackerRepository userTrackerRepository;
private final Course course;
private final PluginMessages pluginMessages;
/**
* Endpoint which generates the report card for the current use to show the stats on the solved
* lessons
*/
@GetMapping(path = "/service/reportcard.mvc", produces = "application/json")
@ResponseBody
public ReportCard reportCard() {
final ReportCard reportCard = new ReportCard();
reportCard.setTotalNumberOfLessons(course.getTotalOfLessons());
reportCard.setTotalNumberOfAssignments(course.getTotalOfAssignments());
UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName());
reportCard.setNumberOfAssignmentsSolved(userTracker.numberOfAssignmentsSolved());
reportCard.setNumberOfLessonsSolved(userTracker.numberOfLessonsSolved());
for (Lesson lesson : course.getLessons()) {
LessonTracker lessonTracker = userTracker.getLessonTracker(lesson);
final LessonStatistics lessonStatistics = new LessonStatistics();
lessonStatistics.setName(pluginMessages.getMessage(lesson.getTitle()));
lessonStatistics.setNumberOfAttempts(lessonTracker.getNumberOfAttempts());
lessonStatistics.setSolved(lessonTracker.isLessonSolved());
reportCard.lessonStatistics.add(lessonStatistics);
}
return reportCard;
}
@Getter
@Setter
private final class ReportCard {
private int totalNumberOfLessons;
private int totalNumberOfAssignments;
private int solvedLessons;
private int numberOfAssignmentsSolved;
private int numberOfLessonsSolved;
private List<LessonStatistics> lessonStatistics = new ArrayList<>();
}
@Setter
@Getter
private final class LessonStatistics {
private String name;
private boolean solved;
private int numberOfAttempts;
}
}

View File

@ -32,8 +32,8 @@ import org.flywaydb.core.Flyway;
import org.owasp.webgoat.container.lessons.Initializeable;
import org.owasp.webgoat.container.lessons.Lesson;
import org.owasp.webgoat.container.session.WebSession;
import org.owasp.webgoat.container.users.UserTracker;
import org.owasp.webgoat.container.users.UserTrackerRepository;
import org.owasp.webgoat.container.users.UserProgress;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@ -45,7 +45,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
public class RestartLessonService {
private final WebSession webSession;
private final UserTrackerRepository userTrackerRepository;
private final UserProgressRepository userTrackerRepository;
private final Function<String, Flyway> flywayLessons;
private final List<Initializeable> lessonsToInitialize;
@ -55,7 +55,7 @@ public class RestartLessonService {
Lesson al = webSession.getCurrentLesson();
log.debug("Restarting lesson: " + al);
UserTracker userTracker = userTrackerRepository.findByUser(webSession.getUserName());
UserProgress userTracker = userTrackerRepository.findByUser(webSession.getUserName());
userTracker.reset(al);
userTrackerRepository.save(userTracker);

View File

@ -52,7 +52,7 @@ import org.owasp.webgoat.container.lessons.Lesson;
*/
@Entity
@EqualsAndHashCode
public class LessonTracker {
public class LessonProgress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ -69,11 +69,11 @@ public class LessonTracker {
@Getter private int numberOfAttempts = 0;
@Version private Integer version;
private LessonTracker() {
protected LessonProgress() {
// JPA
}
public LessonTracker(Lesson lesson) {
public LessonProgress(Lesson lesson) {
lessonName = lesson.getId();
allAssignments.addAll(lesson.getAssignments() == null ? List.of() : lesson.getAssignments());
}
@ -119,4 +119,8 @@ public class LessonTracker {
overview.putAll(solvedAssignments.stream().collect(Collectors.toMap(a -> a, b -> true)));
return overview;
}
long numberOfSolvedAssignments() {
return solvedAssignments.size();
}
}

View File

@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RestController;
@AllArgsConstructor
public class Scoreboard {
private final UserTrackerRepository userTrackerRepository;
private final UserProgressRepository userTrackerRepository;
private final UserRepository userRepository;
private final Course course;
private final PluginMessages pluginMessages;
@ -46,7 +46,7 @@ public class Scoreboard {
.collect(Collectors.toList());
}
private List<String> challengesSolved(UserTracker userTracker) {
private List<String> challengesSolved(UserProgress userTracker) {
List<String> challenges =
List.of(
"Challenge1",
@ -59,10 +59,10 @@ public class Scoreboard {
"Challenge8",
"Challenge9");
return challenges.stream()
.map(userTracker::getLessonTracker)
.map(userTracker::getLessonProgress)
.flatMap(Optional::stream)
.filter(LessonTracker::isLessonSolved)
.map(LessonTracker::getLessonName)
.filter(LessonProgress::isLessonSolved)
.map(LessonProgress::getLessonName)
.map(this::toLessonTitle)
.toList();
}

View File

@ -9,13 +9,10 @@ import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.owasp.webgoat.container.lessons.Assignment;
import org.owasp.webgoat.container.lessons.Lesson;
/**
@ -52,7 +49,7 @@ import org.owasp.webgoat.container.lessons.Lesson;
@Slf4j
@Entity
@EqualsAndHashCode
public class UserTracker {
public class UserProgress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ -62,11 +59,11 @@ public class UserTracker {
private String user;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<LessonTracker> lessonTrackers = new HashSet<>();
private Set<LessonProgress> lessonProgress = new HashSet<>();
private UserTracker() {}
protected UserProgress() {}
public UserTracker(final String user) {
public UserProgress(final String user) {
this.user = user;
}
@ -76,15 +73,15 @@ public class UserTracker {
* @param lesson the lesson
* @return a lesson tracker created if not already present
*/
public LessonTracker getLessonTracker(Lesson lesson) {
Optional<LessonTracker> lessonTracker =
lessonTrackers.stream().filter(l -> l.getLessonName().equals(lesson.getId())).findFirst();
if (!lessonTracker.isPresent()) {
LessonTracker newLessonTracker = new LessonTracker(lesson);
lessonTrackers.add(newLessonTracker);
public LessonProgress getLessonProgress(Lesson lesson) {
Optional<LessonProgress> progress =
lessonProgress.stream().filter(l -> l.getLessonName().equals(lesson.getId())).findFirst();
if (!progress.isPresent()) {
LessonProgress newLessonTracker = new LessonProgress(lesson);
lessonProgress.add(newLessonTracker);
return newLessonTracker;
} else {
return lessonTracker.get();
return progress.get();
}
}
@ -94,43 +91,34 @@ public class UserTracker {
* @param id the id of the lesson
* @return optional due to the fact we can only create a lesson tracker based on a lesson
*/
public Optional<LessonTracker> getLessonTracker(String id) {
return lessonTrackers.stream().filter(l -> l.getLessonName().equals(id)).findFirst();
public Optional<LessonProgress> getLessonProgress(String id) {
return lessonProgress.stream().filter(l -> l.getLessonName().equals(id)).findFirst();
}
public void assignmentSolved(Lesson lesson, String assignmentName) {
LessonTracker lessonTracker = getLessonTracker(lesson);
lessonTracker.incrementAttempts();
lessonTracker.assignmentSolved(assignmentName);
LessonProgress progress = getLessonProgress(lesson);
progress.incrementAttempts();
progress.assignmentSolved(assignmentName);
}
public void assignmentFailed(Lesson lesson) {
LessonTracker lessonTracker = getLessonTracker(lesson);
lessonTracker.incrementAttempts();
LessonProgress progress = getLessonProgress(lesson);
progress.incrementAttempts();
}
public void reset(Lesson al) {
LessonTracker lessonTracker = getLessonTracker(al);
lessonTracker.reset();
LessonProgress progress = getLessonProgress(al);
progress.reset();
}
public int numberOfLessonsSolved() {
int numberOfLessonsSolved = 0;
for (LessonTracker lessonTracker : lessonTrackers) {
if (lessonTracker.isLessonSolved()) {
numberOfLessonsSolved = numberOfLessonsSolved + 1;
}
}
return numberOfLessonsSolved;
public long numberOfLessonsSolved() {
return lessonProgress.stream().filter(LessonProgress::isLessonSolved).count();
}
public int numberOfAssignmentsSolved() {
int numberOfAssignmentsSolved = 0;
for (LessonTracker lessonTracker : lessonTrackers) {
Map<Assignment, Boolean> lessonOverview = lessonTracker.getLessonOverview();
numberOfAssignmentsSolved =
lessonOverview.values().stream().filter(b -> b).collect(Collectors.counting()).intValue();
}
return numberOfAssignmentsSolved;
public long numberOfAssignmentsSolved() {
return lessonProgress.stream()
.map(LessonProgress::numberOfSolvedAssignments)
.mapToLong(Long::valueOf)
.sum();
}
}

View File

@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
* @author nbaars
* @since 4/30/17.
*/
public interface UserTrackerRepository extends JpaRepository<UserTracker, String> {
public interface UserProgressRepository extends JpaRepository<UserProgress, String> {
UserTracker findByUser(String user);
UserProgress findByUser(String user);
}

View File

@ -19,7 +19,7 @@ import org.springframework.stereotype.Service;
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final UserTrackerRepository userTrackerRepository;
private final UserProgressRepository userTrackerRepository;
private final JdbcTemplate jdbcTemplate;
private final Function<String, Flyway> flywayLessons;
private final List<Initializeable> lessonInitializables;
@ -43,7 +43,7 @@ public class UserService implements UserDetailsService {
if (!userAlreadyExists) {
userTrackerRepository.save(
new UserTracker(username)); // if user previously existed it will not get another tracker
new UserProgress(username)); // if user previously existed it will not get another tracker
createLessonsForUser(webGoatUser);
}
}

View File

@ -26,8 +26,8 @@ import jakarta.servlet.http.HttpServletRequest;
import org.owasp.webgoat.container.assignments.AssignmentEndpoint;
import org.owasp.webgoat.container.assignments.AssignmentHints;
import org.owasp.webgoat.container.assignments.AttackResult;
import org.owasp.webgoat.container.users.UserTracker;
import org.owasp.webgoat.container.users.UserTrackerRepository;
import org.owasp.webgoat.container.users.UserProgress;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@ -40,9 +40,9 @@ import org.springframework.web.bind.annotation.RestController;
@AssignmentHints({"csrf-login-hint1", "csrf-login-hint2", "csrf-login-hint3"})
public class CSRFLogin extends AssignmentEndpoint {
private final UserTrackerRepository userTrackerRepository;
private final UserProgressRepository userTrackerRepository;
public CSRFLogin(UserTrackerRepository userTrackerRepository) {
public CSRFLogin(UserProgressRepository userTrackerRepository) {
this.userTrackerRepository = userTrackerRepository;
}
@ -60,7 +60,7 @@ public class CSRFLogin extends AssignmentEndpoint {
}
private void markAssignmentSolvedWithRealUser(String username) {
UserTracker userTracker = userTrackerRepository.findByUser(username);
UserProgress userTracker = userTrackerRepository.findByUser(username);
userTracker.assignmentSolved(
getWebSession().getCurrentLesson(), this.getClass().getSimpleName());
userTrackerRepository.save(userTracker);