diff --git a/pom.xml b/pom.xml
index a40ea268..d057c65e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -138,6 +138,12 @@
${cucumber.version}
test
+
+ io.cucumber
+ cucumber-spring
+ ${cucumber.version}
+ test
+
diff --git a/qodana.yaml b/qodana.yaml
new file mode 100644
index 00000000..34dc3363
--- /dev/null
+++ b/qodana.yaml
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------------#
+# Qodana analysis is configured by qodana.yaml file #
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
+#-------------------------------------------------------------------------------#
+version: "1.0"
+
+#Specify inspection profile for code analysis
+profile:
+ name: qodana.starter
+
+#Enable inspections
+#include:
+# - name:
+
+#Disable inspections
+#exclude:
+# - name:
+# paths:
+# -
+
+projectJDK: 21 #(Applied in CI/CD pipeline)
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)
+
+#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
+linter: jetbrains/qodana-jvm-community:latest
diff --git a/src/main/java/fr/insee/genesis/Constants.java b/src/main/java/fr/insee/genesis/Constants.java
index e4913b28..80cda3a5 100644
--- a/src/main/java/fr/insee/genesis/Constants.java
+++ b/src/main/java/fr/insee/genesis/Constants.java
@@ -10,11 +10,13 @@ public class Constants {
"(^([0-9]|[0-2][0-9]|3[0-1])[\\-\\/]([0-9]|1[0-2]|0[1-9])[\\-\\/]([0-9]{4})$)";
public static final String FILTER_RESULT_PREFIX = "FILTER_RESULT_";
public static final String MISSING_SUFFIX = "_MISSING";
+ public static final String MONGODB_LUNATIC_RAWDATA_COLLECTION_NAME = "lunaticjsondata";
private static final String[] ENO_VARIABLES = {"COMMENT_QE","COMMENT_UE","HEURE_REMPL","MIN_REMPL"};
public static final String MONGODB_SCHEDULE_COLLECTION_NAME = "schedules";
public static final String LOOP_NAME_PREFIX = "BOUCLE";
public static final String MONGODB_RESPONSE_COLLECTION_NAME = "responses";
+ public static final String MONGODB_VARIABLETYPE_COLLECTION_NAME = "variabletypes";
public static final String VOLUMETRY_FOLDER_NAME = "genesis_volumetries";
public static final String VOLUMETRY_FILE_SUFFIX = "_VOLUMETRY";
public static final String VOLUMETRY_FILE_DATE_FORMAT = "yyyy_MM_dd";
diff --git a/src/main/java/fr/insee/genesis/configuration/Config.java b/src/main/java/fr/insee/genesis/configuration/Config.java
index f3e83444..15d51174 100644
--- a/src/main/java/fr/insee/genesis/configuration/Config.java
+++ b/src/main/java/fr/insee/genesis/configuration/Config.java
@@ -2,12 +2,14 @@
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import java.nio.file.Path;
@Configuration
@Getter
+@EnableCaching
public class Config {
/******************************************************/
diff --git a/src/main/java/fr/insee/genesis/configuration/auth/security/ApplicationRole.java b/src/main/java/fr/insee/genesis/configuration/auth/security/ApplicationRole.java
new file mode 100644
index 00000000..212749e8
--- /dev/null
+++ b/src/main/java/fr/insee/genesis/configuration/auth/security/ApplicationRole.java
@@ -0,0 +1,10 @@
+package fr.insee.genesis.configuration.auth.security;
+
+public enum ApplicationRole {
+ ADMIN,
+ USER_KRAFTWERK,
+ USER_PLATINE,
+ COLLECT_PLATFORM,
+ READER
+}
+
diff --git a/src/main/java/fr/insee/genesis/configuration/auth/security/OIDCSecurityConfig.java b/src/main/java/fr/insee/genesis/configuration/auth/security/OIDCSecurityConfig.java
index ac524ce0..fa7b9635 100644
--- a/src/main/java/fr/insee/genesis/configuration/auth/security/OIDCSecurityConfig.java
+++ b/src/main/java/fr/insee/genesis/configuration/auth/security/OIDCSecurityConfig.java
@@ -1,48 +1,120 @@
package fr.insee.genesis.configuration.auth.security;
-import fr.insee.genesis.configuration.Config;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
@Configuration
@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
@Slf4j
@ConditionalOnProperty(name = "fr.insee.genesis.authentication", havingValue = "OIDC")
+@ConfigurationProperties(prefix = "fr.insee.genesis.security")
+@RequiredArgsConstructor
public class OIDCSecurityConfig {
- Config config;
- @Autowired
- public OIDCSecurityConfig(Config config) {
- this.config = config;
- }
+ @Getter
+ @Setter
+ private String[] whitelistMatchers;
+ private static final String ROLE_PREFIX = "ROLE_";
+ private final RoleConfiguration roleConfiguration;
+ private final SecurityTokenProperties inseeSecurityTokenProperties;
- @Bean
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- http
- .csrf(AbstractHttpConfigurer::disable)
- .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
- for (var pattern : config.getWhiteList()) {
- http.authorizeHttpRequests(authorize ->
- authorize
- .requestMatchers(AntPathRequestMatcher.antMatcher(pattern)).permitAll()
- );
- }
- http
- .authorizeHttpRequests(configurer -> configurer
- .anyRequest().authenticated()
- )
- .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
- return http.build();
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+ for (var pattern : whitelistMatchers) {
+ http.authorizeHttpRequests(authorize ->
+ authorize
+ .requestMatchers(AntPathRequestMatcher.antMatcher(pattern)).permitAll()
+ );
}
+ http
+ .authorizeHttpRequests(configurer -> configurer
+ .requestMatchers(HttpMethod.GET,"/questionnaires/**").hasRole(String.valueOf(ApplicationRole.READER))
+ .requestMatchers(HttpMethod.GET,"/modes/**").hasRole(String.valueOf(ApplicationRole.READER))
+ .requestMatchers(HttpMethod.GET,"/interrogations/**").hasRole(String.valueOf(ApplicationRole.READER))
+ .requestMatchers(HttpMethod.GET,"/campaigns/**").hasRole(String.valueOf(ApplicationRole.READER))
+ .anyRequest().authenticated()
+ )
+ .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
+ return http.build();
+ }
+
+ @Bean
+ JwtAuthenticationConverter jwtAuthenticationConverter() {
+ JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+ jwtAuthenticationConverter.setPrincipalClaimName(inseeSecurityTokenProperties.getOidcClaimUsername());
+ jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
+ return jwtAuthenticationConverter;
+ }
+
+ Converter> jwtGrantedAuthoritiesConverter() {
+ return new Converter>() {
+ @Override
+ @SuppressWarnings({"unchecked"})
+ public Collection convert(Jwt source) {
+
+ String[] claimPath = inseeSecurityTokenProperties.getOidcClaimRole().split("\\.");
+ Map claims = source.getClaims();
+ try {
+ for (int i = 0; i < claimPath.length - 1; i++) {
+ claims = (Map) claims.get(claimPath[i]);
+ }
+ if (claims != null) {
+ List tokenClaims = (List) claims.getOrDefault(claimPath[claimPath.length - 1], List.of());
+ // Collect distinct values from mapping associated with input keys
+ List claimedRoles = tokenClaims.stream()
+ .filter(roleConfiguration.getRolesByClaim()::containsKey) // Ensure the key exists in the mapping
+ .flatMap(key -> roleConfiguration.getRolesByClaim().get(key).stream()) // Get the list of values associated with the key
+ .distinct() // Remove duplicates
+ .toList();
+
+ return Collections.unmodifiableCollection(claimedRoles.stream().map(s -> new GrantedAuthority() {
+ @Override
+ public String getAuthority() {
+ return ROLE_PREFIX + s;
+ }
+
+ @Override
+ public String toString() {
+ return getAuthority();
+ }
+ }).toList());
+ }
+ } catch (ClassCastException e) {
+ // role path not correctly found, assume that no role for this user
+ return List.of();
+ }
+ return List.of();
+ }
+ };
+ }
}
diff --git a/src/main/java/fr/insee/genesis/configuration/auth/security/RoleConfiguration.java b/src/main/java/fr/insee/genesis/configuration/auth/security/RoleConfiguration.java
new file mode 100644
index 00000000..67b017da
--- /dev/null
+++ b/src/main/java/fr/insee/genesis/configuration/auth/security/RoleConfiguration.java
@@ -0,0 +1,97 @@
+package fr.insee.genesis.configuration.auth.security;
+
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Configuration
+@Slf4j
+public class RoleConfiguration {
+
+ //Mapping des claims du jeton sur les roles applicatifs
+ @Value("#{'${app.role.admin.claims}'.split(',')}")
+ private List adminClaims;
+ @Value("#{'${app.role.user-kraftwerk.claims}'.split(',')}")
+ private List userKraftwerkClaims;
+ @Value("#{'${app.role.user-platine.claims}'.split(',')}")
+ private List userPlatineClaims;
+ @Value("#{'${app.role.reader.claims}'.split(',')}")
+ private List readerClaims;
+ @Value("#{'${app.role.collect-platform.claims}'.split(',')}")
+ private List collectPlatformClaims;
+
+ public Map> getRolesByClaim() {
+ return rolesByClaim;
+ }
+
+ private Map> rolesByClaim;
+
+ //Defines a role hierarchy
+ //ADMIN implies USER role too
+ //USER implies READER role too
+ //so an admin has 2 roles: ADMIN/USER
+ @Bean
+ static RoleHierarchy roleHierarchy() {
+ return RoleHierarchyImpl.withDefaultRolePrefix()
+ .role(ApplicationRole.ADMIN.toString()).implies(ApplicationRole.USER_KRAFTWERK.toString())
+ .role(ApplicationRole.ADMIN.toString()).implies(ApplicationRole.USER_PLATINE.toString())
+ .role(ApplicationRole.ADMIN.toString()).implies(ApplicationRole.COLLECT_PLATFORM.toString())
+ .role(ApplicationRole.USER_KRAFTWERK.toString()).implies(ApplicationRole.READER.toString())
+ .role(ApplicationRole.USER_PLATINE.toString()).implies(ApplicationRole.READER.toString())
+ .build();
+ }
+
+ // and, if using pre-post method security also add
+ @Bean
+ static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
+ DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
+ expressionHandler.setRoleHierarchy(roleHierarchy);
+ return expressionHandler;
+ }
+
+ @PostConstruct
+ public void initialization() {
+
+ rolesByClaim = new HashMap<>();
+
+ // Ajout des claims pour le rôle ADMIN
+ adminClaims.forEach(claim -> rolesByClaim
+ .computeIfAbsent(claim, k -> new ArrayList<>())
+ .add(String.valueOf(ApplicationRole.ADMIN)));
+
+ // Ajout des claims pour le rôle USER_KRAFTWERK
+ userKraftwerkClaims.forEach(claim -> rolesByClaim
+ .computeIfAbsent(claim, k -> new ArrayList<>())
+ .add(String.valueOf(ApplicationRole.USER_KRAFTWERK)));
+
+ // Ajout des claims pour le rôle USER_PLATINE
+ userPlatineClaims.forEach(claim -> rolesByClaim
+ .computeIfAbsent(claim, k -> new ArrayList<>())
+ .add(String.valueOf(ApplicationRole.USER_PLATINE)));
+
+ // Ajout des claims pour le rôle COLLECT_PLATFORM
+ collectPlatformClaims.forEach(claim -> rolesByClaim
+ .computeIfAbsent(claim, k -> new ArrayList<>())
+ .add(String.valueOf(ApplicationRole.COLLECT_PLATFORM)));
+
+ // Ajout des claims pour le rôle READER
+ readerClaims.forEach(claim -> rolesByClaim
+ .computeIfAbsent(claim, k -> new ArrayList<>())
+ .add(String.valueOf(ApplicationRole.READER)));
+
+
+ log.info("Roles configuration : {}", rolesByClaim);
+ }
+
+}
diff --git a/src/main/java/fr/insee/genesis/configuration/auth/security/SecurityTokenProperties.java b/src/main/java/fr/insee/genesis/configuration/auth/security/SecurityTokenProperties.java
new file mode 100644
index 00000000..1990b957
--- /dev/null
+++ b/src/main/java/fr/insee/genesis/configuration/auth/security/SecurityTokenProperties.java
@@ -0,0 +1,19 @@
+package fr.insee.genesis.configuration.auth.security;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(
+ prefix = "fr.insee.genesis.security.token"
+)
+@Data
+public class SecurityTokenProperties {
+
+ //Chemin pour récupérer la liste des rôles dans le jwt (token)
+ private String oidcClaimRole;
+ //Chemin pour récupérer le username dans le jwt (token)
+ private String oidcClaimUsername;
+
+}
diff --git a/src/main/java/fr/insee/genesis/controller/dto/rawdata/LunaticJsonRawDataUnprocessedDto.java b/src/main/java/fr/insee/genesis/controller/dto/rawdata/LunaticJsonRawDataUnprocessedDto.java
new file mode 100644
index 00000000..8e1012a7
--- /dev/null
+++ b/src/main/java/fr/insee/genesis/controller/dto/rawdata/LunaticJsonRawDataUnprocessedDto.java
@@ -0,0 +1,6 @@
+package fr.insee.genesis.controller.dto.rawdata;
+
+import lombok.Builder;
+
+@Builder
+public record LunaticJsonRawDataUnprocessedDto(String campaignId, String interrogationId){}
\ No newline at end of file
diff --git a/src/main/java/fr/insee/genesis/controller/rest/ScheduleController.java b/src/main/java/fr/insee/genesis/controller/rest/ScheduleController.java
index 9be6b1e7..b238d8f2 100644
--- a/src/main/java/fr/insee/genesis/controller/rest/ScheduleController.java
+++ b/src/main/java/fr/insee/genesis/controller/rest/ScheduleController.java
@@ -15,6 +15,7 @@
import io.swagger.v3.oas.annotations.Parameter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -48,6 +49,7 @@ public ScheduleController(ScheduleApiPort scheduleApiPort, FileUtils fileUtils)
@Operation(summary = "Fetch all schedules")
@GetMapping(path = "/all")
+ @PreAuthorize("hasRole('READER')")
public ResponseEntity