rajah il y a 11 mois
Parent
commit
3203e0d8f1

BIN
.gradle/8.11.1/checksums/checksums.lock


BIN
.gradle/8.11.1/executionHistory/executionHistory.bin


BIN
.gradle/8.11.1/executionHistory/executionHistory.lock


BIN
.gradle/8.11.1/fileHashes/fileHashes.bin


BIN
.gradle/8.11.1/fileHashes/fileHashes.lock


BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock


BIN
.gradle/file-system.probe


+ 4 - 1
bin/main/application.properties

@@ -6,6 +6,8 @@ spring.datasource.password=Atari$Impact2024
 spring.jpa.hibernate.ddl-auto=validate
 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 
+spring.messages.basename=messages,langs.messages
+
 #spring.jpa.show-sql=true
 
 logging.level.org.springframework=INFO
@@ -20,4 +22,5 @@ server.servlet.context-path=/demovote-api/v1
 #logging.logback.rollingpolicy.max-history=5
 
 jwttoken.secret=ee3c5233e2fde173cf7f401e5fb45aa47937a76f45e5fdcff29bedba6e6ea61c695ac0058ead08561261445b6f547aced2e335c2cc210fab42bc4b5317f987e9297b5c0e19eb21f38d0fd5cf69ba4cfa7ed0fa02d299a34ed6fdf22b508997a573075c4c375e6f3e45c7cb82c78958b2f3d47a87145eb74334023429401f584928a224796093afad62696dc9bab1cfdf4368a2263a13480b80faf873ca1f1cb067da4db75ec53379e0da1d3a61572dbeebfc3484f6f2ed333c96154036d0c22a5a2a59895ee6711e77e604e8b8c5b0a45fb2cce05298d12c25e1f9a6ba4d030ce2e480c1e3ad3fe0551c2a136bd18635c829f7eb4f92f4e34ec67e95bb966dac
-jwttoken.expiration=3600000 
+jwttoken.expiration=3600000 
+

+ 4 - 1
src/main/java/fr/triplea/demovote/CreateDefaultValues.java

@@ -2,6 +2,7 @@ package fr.triplea.demovote;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationListener;
@@ -38,7 +39,9 @@ public class CreateDefaultValues implements ApplicationListener<ContextRefreshed
   public void onApplicationEvent(ContextRefreshedEvent event) 
   {
     if (initialise) { return; } 
-    
+
+    Locale.setDefault(Locale.ENGLISH);
+      
     Role adminRole = addRoleIfMissing("ROLE_ADMIN");
     Role orgaRole = addRoleIfMissing("ROLE_ORGA");
     Role userRole = addRoleIfMissing("ROLE_USER");

+ 22 - 1
src/main/java/fr/triplea/demovote/dao/ParticipantRepository.java

@@ -69,7 +69,28 @@ public interface ParticipantRepository extends JpaRepository<Participant, Intege
       + "AND ((:statut = 0) OR (:statut = 1 AND p.statut = 'EN_ATTENTE'::vote.statut_participant)) "
       + "AND ((:arrive = 0) OR (:arrive = 1 AND p.flag_arrive = FALSE) OR (:arrive = 2 AND p.flag_arrive = TRUE)) "
       + "ORDER BY p.nom ASC, p.prenom ASC, p.pseudonyme ASC ")
-  List<ParticipantList> getList(@Param("nom") String nom, @Param("statut") int statut, @Param("arrive") int arrive);
+  List<ParticipantList> getListOrderedByNom(@Param("nom") String nom, @Param("statut") int statut, @Param("arrive") int arrive);
+
+  @NativeQuery("SELECT DISTINCT "
+      + "p.numero_participant, "
+      + "p.nom, "
+      + "p.prenom, "
+      + "p.pseudonyme, "
+      + "p.groupe, "
+      + "p.email, "
+      + "p.statut, "
+      + "p.flag_jour1, "
+      + "p.flag_jour2, "
+      + "p.flag_jour3, "
+      + "p.flag_dodo_sur_place, "
+      + "p.flag_arrive "
+      + "FROM vote.participants AS p "
+      + "WHERE p.flag_actif IS TRUE "
+      + "AND ((:nom is null) OR (UPPER(p.nom) LIKE CONCAT('%', :nom, '%')) OR (UPPER(p.prenom) LIKE CONCAT('%', :nom, '%')) OR (UPPER(p.pseudonyme) LIKE CONCAT('%', :nom, '%')) OR (UPPER(p.groupe) LIKE CONCAT('%', :nom, '%')) OR (UPPER(p.email) LIKE CONCAT('%', :nom, '%'))) "
+      + "AND ((:statut = 0) OR (:statut = 1 AND p.statut = 'EN_ATTENTE'::vote.statut_participant)) "
+      + "AND ((:arrive = 0) OR (:arrive = 1 AND p.flag_arrive = FALSE) OR (:arrive = 2 AND p.flag_arrive = TRUE)) "
+      + "ORDER BY p.numero_participant ASC ")
+  List<ParticipantList> getListOrderedByDateInscription(@Param("nom") String nom, @Param("statut") int statut, @Param("arrive") int arrive);
 
   @NativeQuery("SELECT DISTINCT p.* FROM vote.participants AS p WHERE p.flag_actif IS TRUE ORDER BY p.nom ASC, p.prenom ASC, p.pseudonyme ASC ")
   List<Participant> findAll();

+ 5 - 5
src/main/java/fr/triplea/demovote/security/JwtAuthenticationEntryPoint.java

@@ -8,18 +8,18 @@ import org.springframework.stereotype.Component;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
 
 @Component
 public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint 
 {
-  @SuppressWarnings("unused") 
-  private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
+  //@SuppressWarnings("unused") 
+  //private static final Logger LOG = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
   
   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException 
   {
-    //logger.error("Unauthorized access error : " + authException.getMessage());
+    //LOG.error("Unauthorized access error : " + authException.getMessage());
     
     response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized Access");
   }

+ 31 - 5
src/main/java/fr/triplea/demovote/security/SecurityConfig.java

@@ -3,6 +3,7 @@ package fr.triplea.demovote.security;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.ResourceBundleMessageSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -15,17 +16,32 @@ import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
+import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 import org.springframework.security.web.context.DelegatingSecurityContextRepository;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
 
 @Configuration
+@EnableWebMvc
 @EnableMethodSecurity
 public class SecurityConfig
 {
  
-  // TODO: CSRF-TOKEN, filtrage anti-XSS, filtrage anti-SQL-injection, etc
-  // TODO: gérer le 403 au niveau du frontend (en cas d'expiration du JWT)
+  // TODO: CSRF-TOKEN
+  // TODO: gérer le 403 au niveau du frontend (en cas d'expiration du JWT -> refreshToken)
+
+  @Bean
+  public ResourceBundleMessageSource messageSource() 
+  {
+    var source = new ResourceBundleMessageSource();
+  
+    source.setBasenames("langs/messages");
+    source.setUseCodeAsDefaultMessage(true);
+
+    return source;
+  }
 
   @Autowired
   private MyUserDetailsService myUserDetailsService;
@@ -61,12 +77,15 @@ public class SecurityConfig
   
   Class<? extends UsernamePasswordAuthenticationFilter> clazz = UsernamePasswordAuthenticationFilter.class;
 
+  @Bean
+  public XSSFilter xssFilter() { return new XSSFilter(); }
+   
   @Bean
   SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityContextRepository securityContextRepository) throws Exception 
   {
     http.csrf(csrf -> csrf.disable())
         .authenticationProvider(authenticationProvider())
-        .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
+        .authorizeHttpRequests((ahreq) -> ahreq
           .requestMatchers("/divers/**", "/sign/**").permitAll()
           .requestMatchers("/account/**", "/preference/**", "/message/**", "/urne/**", "/resultats/**").hasRole("USER")
           .requestMatchers("/variable/**", "/categorie/**", "/production/**", "/presentation/**").hasRole("ADMIN")
@@ -74,8 +93,15 @@ public class SecurityConfig
           .anyRequest().authenticated()
           )
         .addFilterBefore(jwtTokenFilter(), clazz)
-        .securityContext(securityContext -> securityContext.securityContextRepository(securityContextRepository).requireExplicitSave(true))
-        .headers(headers -> headers.frameOptions(customize -> customize.disable()))
+        .securityContext(sc -> sc.securityContextRepository(securityContextRepository).requireExplicitSave(true))
+        .headers(headers -> headers
+          .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
+          .contentSecurityPolicy(csp -> csp.policyDirectives("script-src 'self'"))
+          .frameOptions(fopt -> fopt.sameOrigin())
+          .cacheControl(cache -> cache.disable())
+          .httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).preload(true).maxAgeInSeconds(31536000))
+          .referrerPolicy(referrer -> referrer.policy(ReferrerPolicy.SAME_ORIGIN))
+          )
         .sessionManagement(session -> session.maximumSessions(2).sessionRegistry(sessionRegistry()))
         ;
         

+ 26 - 0
src/main/java/fr/triplea/demovote/security/XSSFilter.java

@@ -0,0 +1,26 @@
+package fr.triplea.demovote.security;
+
+import java.io.IOException;
+
+import org.springframework.core.annotation.Order;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.annotation.WebFilter;
+import jakarta.servlet.http.HttpServletRequest;
+
+@WebFilter("/*")
+@Order(1)
+public class XSSFilter implements Filter 
+{
+  
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
+  {
+    chain.doFilter(new XSSRequestWrapper((HttpServletRequest) request), response);
+  }
+
+}

+ 118 - 0
src/main/java/fr/triplea/demovote/security/XSSRequestWrapper.java

@@ -0,0 +1,118 @@
+package fr.triplea.demovote.security;
+
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class XSSRequestWrapper extends HttpServletRequestWrapper 
+{
+  //@SuppressWarnings("unused") 
+  //private static final Logger LOG = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
+
+  public XSSRequestWrapper(HttpServletRequest request) { super(request); }
+
+  @Override
+  public String[] getParameterValues(String parameter) 
+  {
+    String[] values = super.getParameterValues(parameter);
+    
+    if (values != null) { for (int i = 0; i < values.length; i++) { values[i] = XssSanitizerUtil.stripXSS(values[i]); } }
+
+    return values;
+  }
+
+  @Override 
+  public String getParameter(String parameter) 
+  {
+    String value = super.getParameter(parameter);
+    
+    if (value != null) { value = XssSanitizerUtil.stripXSS(value); }
+
+    return value;  
+  }
+
+  @Override
+  public String getHeader(String name) 
+  {
+    String value = super.getHeader(name);
+   
+    if (value != null) { value = XssSanitizerUtil.stripXSS(value); }
+
+    return value;  
+  }
+
+  @Override
+  public ServletInputStream getInputStream() throws IOException 
+  {
+    ServletInputStream inputStream = super.getInputStream();
+   
+    String requestBody = new String(inputStream.readAllBytes());
+  
+    ObjectMapper mapper = new ObjectMapper();
+  
+    JsonNode jsonNode = mapper.readTree(requestBody);
+  
+    sanitizeJsonNode(jsonNode);
+  
+    String sanitizeBody = mapper.writeValueAsString(jsonNode);
+
+    return new ServletInputStream() 
+    {
+      private final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(sanitizeBody.getBytes());
+
+      @Override
+      public int read() throws IOException { return byteArrayInputStream.read(); }
+
+      @Override
+      public boolean isReady() { return true; }
+
+      @Override
+      public boolean isFinished() { return byteArrayInputStream.available() == 0; }
+
+      @Override
+      public void setReadListener(ReadListener readListener) { }
+    };
+  }
+
+
+  private void sanitizeJsonNode(JsonNode node) 
+  {
+    if (node.isObject()) 
+    {
+      ObjectNode objectNode = (ObjectNode) node;
+    
+      objectNode.fields().forEachRemaining(entry -> 
+      {
+        JsonNode valueNode = entry.getValue();
+        
+        if (valueNode.isTextual()) { objectNode.put(entry.getKey(), XssSanitizerUtil.stripXSS(valueNode.textValue())); } 
+        else 
+        if (valueNode.isObject() || valueNode.isArray()) { sanitizeJsonNode(valueNode); }
+      });
+    } 
+    else if (node.isArray()) 
+    {
+      ArrayNode arrayNode = (ArrayNode) node;
+    
+      for (int i=0 ; i< arrayNode.size(); i++) 
+      {
+        JsonNode jsonNode = arrayNode.get(i);
+      
+        if (jsonNode.isObject()) { sanitizeJsonNode(jsonNode); } else if (jsonNode.isTextual()) { arrayNode.set(i, XssSanitizerUtil.stripXSS(jsonNode.textValue())); }
+      }
+    }
+  }
+  
+}

+ 48 - 0
src/main/java/fr/triplea/demovote/security/XssSanitizerUtil.java

@@ -0,0 +1,48 @@
+package fr.triplea.demovote.security;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.springframework.web.util.HtmlUtils;
+
+public class XssSanitizerUtil 
+{
+  
+  private XssSanitizerUtil() {}
+  
+  private static List<Pattern> patterns = new ArrayList<Pattern>();
+  
+  static 
+  {
+    patterns.add(Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE));
+    patterns.add(Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("</script>", Pattern.CASE_INSENSITIVE));
+    patterns.add(Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("<input(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE));
+    patterns.add(Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE));
+    patterns.add(Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("onfocus(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+    patterns.add(Pattern.compile("<form[^>]*>.*?</form>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL));
+  }
+
+  private static Pattern htmlPattern = Pattern.compile("<(\"[^\"]*\"|'[^']*'|[^'\">])*>", Pattern.MULTILINE);
+  
+  public static String stripXSS(String value) 
+  {
+    if (value != null) 
+    { 
+      value = value.replaceAll("\0", "");
+      
+      for (Pattern pattern : patterns) { value = pattern.matcher(value).replaceAll(""); } 
+      
+      if (htmlPattern.matcher(value).find()) { value = HtmlUtils.htmlEscape(value); }
+    }
+    return value;
+  }
+  
+}

+ 13 - 2
src/main/java/fr/triplea/demovote/web/controller/AuthController.java

@@ -1,10 +1,12 @@
 package fr.triplea.demovote.web.controller;
 
 import java.util.List;
+import java.util.Locale;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.LocaleResolver;
 
 import fr.triplea.demovote.dao.ParticipantRepository;
 import fr.triplea.demovote.dto.UserCredentials;
@@ -47,6 +50,12 @@ public class AuthController
   @Autowired
   private ParticipantRepository participantRepository;
 
+  @Autowired
+  private LocaleResolver localeResolver;
+  
+  @Autowired
+  private MessageSource messageSource;
+  
   
   @PostMapping(value = "/in")
   public ResponseEntity<UserCredentials> signIn(@RequestBody UserCredentials uc, HttpServletRequest request, HttpServletResponse response)
@@ -58,6 +67,8 @@ public class AuthController
     
     Participant found = participantRepository.findByPseudonyme(usrn);
     
+    Locale locale = localeResolver.resolveLocale(request);
+    
     if (found != null)
     { 
       UserDetails userDetails = myUserDetailsService.loadUserByUsername(usrn);
@@ -99,7 +110,7 @@ public class AuthController
         uc.setPrenom("");
         uc.setToken("");
         uc.setRole("");
-        uc.setErreur("Le mot de passe ne correspond pas à ce participant.");
+        uc.setErreur(messageSource.getMessage("auth.password.mismatches", null, locale));
        
         return ResponseEntity.ok(uc);
       }
@@ -113,7 +124,7 @@ public class AuthController
     uc.setPrenom("");
     uc.setToken("");
     uc.setRole("");
-    uc.setErreur("Participant introuvable avec ce pseudonyme.");
+    uc.setErreur(messageSource.getMessage("auth.user.notfound", null, locale));
    
     return ResponseEntity.ok(uc);
   }

+ 4 - 2
src/main/java/fr/triplea/demovote/web/controller/ParticipantController.java

@@ -47,11 +47,13 @@ public class ParticipantController
 
   @GetMapping(value = "/list")
   @PreAuthorize("hasRole('ORGA')")
-  public List<ParticipantList> getList(@RequestParam("nom") String filtreNom, @RequestParam("statut") int filtreStatut, @RequestParam("arrive") int filtreArrive) 
+  public List<ParticipantList> getList(@RequestParam("nom") String filtreNom, @RequestParam("statut") int filtreStatut, @RequestParam("arrive") int filtreArrive, @RequestParam("tri") int tri) 
   { 
     if (filtreNom != null) { if (filtreNom.isBlank()) { filtreNom = null; } else { filtreNom = filtreNom.trim().toUpperCase(); } }
     
-    return participantRepository.getList(filtreNom, filtreStatut, filtreArrive);
+    if (tri == 1) { return participantRepository.getListOrderedByDateInscription(filtreNom, filtreStatut, filtreArrive); }
+    
+    return participantRepository.getListOrderedByNom(filtreNom, filtreStatut, filtreArrive);
   }
 
   

+ 4 - 1
src/main/resources/application.properties

@@ -6,6 +6,8 @@ spring.datasource.password=Atari$Impact2024
 spring.jpa.hibernate.ddl-auto=validate
 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 
+spring.messages.basename=messages,langs.messages
+
 #spring.jpa.show-sql=true
 
 logging.level.org.springframework=INFO
@@ -20,4 +22,5 @@ server.servlet.context-path=/demovote-api/v1
 #logging.logback.rollingpolicy.max-history=5
 
 jwttoken.secret=ee3c5233e2fde173cf7f401e5fb45aa47937a76f45e5fdcff29bedba6e6ea61c695ac0058ead08561261445b6f547aced2e335c2cc210fab42bc4b5317f987e9297b5c0e19eb21f38d0fd5cf69ba4cfa7ed0fa02d299a34ed6fdf22b508997a573075c4c375e6f3e45c7cb82c78958b2f3d47a87145eb74334023429401f584928a224796093afad62696dc9bab1cfdf4368a2263a13480b80faf873ca1f1cb067da4db75ec53379e0da1d3a61572dbeebfc3484f6f2ed333c96154036d0c22a5a2a59895ee6711e77e604e8b8c5b0a45fb2cce05298d12c25e1f9a6ba4d030ce2e480c1e3ad3fe0551c2a136bd18635c829f7eb4f92f4e34ec67e95bb966dac
-jwttoken.expiration=3600000 
+jwttoken.expiration=3600000 
+

+ 2 - 0
src/main/resources/langs/messages_en.properties

@@ -0,0 +1,2 @@
+auth.password.mismatches=The password does not match any entrant's password.
+auth.user.notfound=The nickname does not seem to be used by any entrant.

+ 2 - 0
src/main/resources/langs/messages_fr.properties

@@ -0,0 +1,2 @@
+auth.password.mismatches=Le mot de passe ne correspond pas à ce participant.
+auth.user.notfound=Le pseudonyme ne semble pas être celui d'un participant.