Skip to content

Ensure Full Test Coverage #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
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
10 changes: 8 additions & 2 deletions build_and_run_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ else
SEARCH_FEATURE="-DenableSearchFeature=$1"
fi

if [ "$1" == "r" ]; then
docker run -d -p 6379:6379 --name redis_container redis
echo "Redis container is running."
exit 0
fi

cleanup() {
docker stop redis_container > /dev/null 2>&1 || true
docker rm redis_container > /dev/null 2>&1 || true
rm *db > /dev/null 2>&1 || true
}

# Trap the EXIT signal to perform cleanup
trap cleanup EXIT
# trap cleanup EXIT

set -e # Exit immediately if a command exits with a non-zero status.
mvn clean package -Dmaven.test.skip=true
docker run -d -p 6379:6379 --name redis_container redis
docker run -d -p 6379:6379 --name redis_container redis | exit 0
java $SEARCH_FEATURE -jar target/salesmanager-*-SNAPSHOT.jar --spring.redis.host=localhost --spring.redis.port=6379 --spring.redis.mode=standalone --server.port=8086 --spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
19 changes: 18 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,24 @@
<artifactId>spring-data-commons</artifactId>
</dependency>

</dependencies>
<!-- TOTP MFA Dependencies -->
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>

</dependencies>

<build>
<plugins>
Expand Down
87 changes: 60 additions & 27 deletions src/main/java/net/codejava/AppController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
Expand All @@ -28,7 +29,6 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.data.domain.PageRequest; // Add this import statement
import org.springframework.data.domain.Page; // Add this import statement
Expand All @@ -49,6 +49,7 @@
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.time.ZoneId;
import javax.servlet.http.Cookie;

@EnableJpaRepositories(basePackages = "net.codejava")
@Controller
Expand All @@ -64,6 +65,9 @@
@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserRepository userRepository;

// private static final Logger logger = Logger.getLogger(AppController.class.getName());

@Value("${enableSearchFeature}")
Expand All @@ -73,6 +77,18 @@
return this.enableSearchFeature;
}

// Add MFA status to all pages that need it
private void addUserInfoToModel(Model model, Principal principal) {
if (principal != null) {
String username = principal.getName();
User user = userRepository.findByUsername(username);
if (user != null) {
model.addAttribute("mfaEnabled", user.isMfaEnabled());
model.addAttribute("username", username);
}
}
}

private String handleSale(Sale sale, HttpSession session, RedirectAttributes redirectAttributes, Runnable action) {
sale.setEditing(true); // set isEditing to true
action.run();
Expand All @@ -88,7 +104,7 @@
}

@RequestMapping("/")
public String viewHomePage(Model model , Principal principal, @RequestParam(defaultValue = "0") int page, HttpSession session) {
public String viewHomePage(Model model, Principal principal, @RequestParam(defaultValue = "0") int page, HttpSession session) {
String lastSearchQuery = (String) session.getAttribute("lastSearchQuery");
if (lastSearchQuery != null && !lastSearchQuery.isEmpty()) {
session.setAttribute("lastSearchQuery", null); // set lastSearchQuery to null
Expand All @@ -102,37 +118,67 @@
model.addAttribute("listSale", salePage.getContent());
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", salePage.getTotalPages());

// Add user info including MFA status
addUserInfoToModel(model, principal);

return "index";
}

@RequestMapping("/new")
public ModelAndView showNewForm() {
public ModelAndView showNewForm(Principal principal) {
ModelAndView mav = new ModelAndView("new_form");
Sale sale = new Sale();
mav.addObject("sale", sale);
mav.addObject("currentDate", LocalDate.now());
mav.addObject("enableSearchFeature", enableSearchFeature);

// Add user info including MFA status
if (principal != null) {
String username = principal.getName();
User user = userRepository.findByUsername(username);
if (user != null) {
mav.addObject("mfaEnabled", user.isMfaEnabled());
mav.addObject("username", username);
}
}

return mav;
}

@RequestMapping("/edit/{serialNumber}")
public ModelAndView showEditForm(@PathVariable(name = "serialNumber") String serialNumber) {
public ModelAndView showEditForm(@PathVariable(name = "serialNumber") String serialNumber, Principal principal) {
ModelAndView mav = new ModelAndView("edit_form");
Sale sale = dao.get(serialNumber);
sale.setEditing(true);
mav.addObject("sale", sale);
mav.addObject("enableSearchFeature", enableSearchFeature);

// Add user info including MFA status
if (principal != null) {
String username = principal.getName();
User user = userRepository.findByUsername(username);
if (user != null) {
mav.addObject("mfaEnabled", user.isMfaEnabled());
mav.addObject("username", username);
}
}

return mav;
}

@RequestMapping("/search")
public String search(@ModelAttribute("q") String query, Model model, HttpSession session) {
public String search(@ModelAttribute("q") String query, Model model, HttpSession session, Principal principal) {
List<Sale> listSale = dao.search(query);
model.addAttribute("listSale", listSale);

boolean enableSearchFeature = true;
model.addAttribute("enableSearchFeature", enableSearchFeature);
session.setAttribute("lastSearchQuery", query); // save the last search query in the session

// Add user info including MFA status
addUserInfoToModel(model, principal);

return "search";
}

Expand Down Expand Up @@ -165,21 +211,27 @@
}

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginPost(HttpServletRequest request, Model model) {
public String loginPost(HttpServletRequest request, HttpServletResponse response, Model model) {
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean rememberMe = "on".equals(request.getParameter("rememberMe"));

// Authenticate the user
Authentication auth = new UsernamePasswordAuthenticationToken(username, password);
try {
auth = authenticationManager.authenticate(auth);
SecurityContextHolder.getContext().setAuthentication(auth);

if (rememberMe) {
Cookie rememberMeCookie = new Cookie("rememberMe", "true");
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
response.addCookie(rememberMeCookie);

Check warning

Code scanning / CodeQL

Failure to use secure cookies Medium

Cookie is added to response without the 'secure' flag being set.

Copilot Autofix

AI about 2 months ago

To fix the issue, the secure flag must be explicitly set on the rememberMeCookie before adding it to the response. This ensures that the cookie is only transmitted over secure HTTPS connections. The change involves calling the setSecure(true) method on the rememberMeCookie object before the response.addCookie(rememberMeCookie) line.


Suggested changeset 1
src/main/java/net/codejava/AppController.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/net/codejava/AppController.java b/src/main/java/net/codejava/AppController.java
--- a/src/main/java/net/codejava/AppController.java
+++ b/src/main/java/net/codejava/AppController.java
@@ -224,6 +224,7 @@
 			if (rememberMe) {
-				Cookie rememberMeCookie = new Cookie("rememberMe", "true");
-				rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
-				rememberMeCookie.setHttpOnly(true);
-				response.addCookie(rememberMeCookie);
+				Cookie rememberMeCookie = new Cookie("rememberMe", "true");
+				rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
+				rememberMeCookie.setHttpOnly(true);
+				rememberMeCookie.setSecure(true);
+				response.addCookie(rememberMeCookie);
 			}
EOF
@@ -224,6 +224,7 @@
if (rememberMe) {
Cookie rememberMeCookie = new Cookie("rememberMe", "true");
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
response.addCookie(rememberMeCookie);
Cookie rememberMeCookie = new Cookie("rememberMe", "true");
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setSecure(true);
response.addCookie(rememberMeCookie);
}
Copilot is powered by AI and may make mistakes. Always verify output.
}
} catch (BadCredentialsException e) {
model.addAttribute("error", "Invalid username or password.");
return "login";
}

// User is authenticated, redirect to landing page
return "redirect:/";
}

Expand All @@ -200,25 +252,6 @@
return handleSale(sale, session, redirectAttributes, () -> dao.clearRecord(serialNumber));
}

@RequestMapping("/export")
public void exportToCSV(HttpServletResponse response) throws IOException {
response.setContentType("text/csv");
response.setHeader("Content-Disposition", "attachment; filename=sales.csv");
List<Sale> listSale = dao.listAll();
// create a writer
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
// write header line
writer.write("Serial Number, Date, Amount, Item Name");
writer.newLine();
// write data lines
for (Sale sale : listSale) {
String line = String.format("%s, %s, %s, %s", sale.getSerialNumber(), sale.getDate(), sale.getAmount(), sale.getItem());
writer.write(line);
writer.newLine();
}
writer.flush();
}

@PostMapping("/import")
public String uploadFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
try {
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/net/codejava/MfaAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package net.codejava;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class MfaAuthentication extends UsernamePasswordAuthenticationToken {

private boolean mfaAuthenticated;
private User user;

public MfaAuthentication(User user, Object credentials,
Collection<? extends GrantedAuthority> authorities,
boolean mfaAuthenticated) {
super(user.getUsername(), credentials, authorities);
this.user = user;
this.mfaAuthenticated = mfaAuthenticated;
}

public boolean isMfaAuthenticated() {
return mfaAuthenticated;
}

public void setMfaAuthenticated(boolean mfaAuthenticated) {
this.mfaAuthenticated = mfaAuthenticated;
}

public User getUser() {
return user;
}

@Override
public boolean isAuthenticated() {
// Only consider the authentication fully complete if MFA has been verified
// for users who have MFA enabled
if (user.isMfaEnabled()) {
return super.isAuthenticated() && mfaAuthenticated;
}
return super.isAuthenticated();
}
}
56 changes: 56 additions & 0 deletions src/main/java/net/codejava/MfaAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.codejava;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Component
public class MfaAuthenticationFilter extends OncePerRequestFilter {

private static final String MFA_VERIFICATION_URL = "/mfa/verify";
private static final String MFA_SETUP_URL = "/mfa/setup";
private static final String LOGIN_URL = "/login";

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
HttpSession session = request.getSession();

// Skip the filter for login and MFA-related URLs
String requestURI = request.getRequestURI();
if (requestURI.equals(LOGIN_URL) || requestURI.startsWith(MFA_VERIFICATION_URL) ||
requestURI.startsWith(MFA_SETUP_URL) || requestURI.startsWith("/css/") ||
requestURI.startsWith("/js/")) {
filterChain.doFilter(request, response);
return;
}

// Check if the user is authenticated with MFA
if (authentication instanceof MfaAuthentication) {
MfaAuthentication mfaAuthentication = (MfaAuthentication) authentication;

// If MFA is needed but not yet verified
if (mfaAuthentication.getUser().isMfaEnabled() && !mfaAuthentication.isMfaAuthenticated()) {
// Store the requested URL in the session
session.setAttribute("REQUESTED_URL", requestURI);

// Redirect to MFA verification page
response.sendRedirect(MFA_VERIFICATION_URL);
return;
}
}

filterChain.doFilter(request, response);
}
}
Loading
Loading