Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package fr.insee.genesis.configuration.auth.security;

public enum ApplicationRole {
ADMIN,
USER_KRAFTWERK,
USER_PLATINE,
COLLECT_PLATFORM,
READER
}

Original file line number Diff line number Diff line change
@@ -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<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
return new Converter<Jwt, Collection<GrantedAuthority>>() {
@Override
@SuppressWarnings({"unchecked"})
public Collection<GrantedAuthority> convert(Jwt source) {

String[] claimPath = inseeSecurityTokenProperties.getOidcClaimRole().split("\\.");
Map<String, Object> claims = source.getClaims();
try {
for (int i = 0; i < claimPath.length - 1; i++) {
claims = (Map<String, Object>) claims.get(claimPath[i]);
}
if (claims != null) {
List<String> tokenClaims = (List<String>) claims.getOrDefault(claimPath[claimPath.length - 1], List.of());
// Collect distinct values from mapping associated with input keys
List<String> 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();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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<String> adminClaims;
@Value("#{'${app.role.user-kraftwerk.claims}'.split(',')}")
private List<String> userKraftwerkClaims;
@Value("#{'${app.role.user-platine.claims}'.split(',')}")
private List<String> userPlatineClaims;
@Value("#{'${app.role.reader.claims}'.split(',')}")
private List<String> readerClaims;
@Value("#{'${app.role.collect-platform.claims}'.split(',')}")
private List<String> collectPlatformClaims;

public Map<String, List<String>> getRolesByClaim() {
return rolesByClaim;
}

private Map<String, List<String>> 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);
}

}
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,6 +49,7 @@ public ScheduleController(ScheduleApiPort scheduleApiPort, FileUtils fileUtils)

@Operation(summary = "Fetch all schedules")
@GetMapping(path = "/all")
@PreAuthorize("hasRole('READER')")
public ResponseEntity<Object> getAllSchedules() {
log.debug("Got GET all schedules request");

Expand All @@ -59,6 +61,7 @@ public ResponseEntity<Object> getAllSchedules() {

@Operation(summary = "Schedule a Kraftwerk execution")
@PutMapping(path = "/create")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Object> addSchedule(
@Parameter(description = "Survey name to call Kraftwerk on") @RequestParam("surveyName") String surveyName,
@Parameter(description = "Kraftwerk endpoint") @RequestParam(value = "serviceTocall", defaultValue = Constants.KRAFTWERK_MAIN_ENDPOINT) ServiceToCall serviceToCall,
Expand Down Expand Up @@ -106,6 +109,7 @@ public ResponseEntity<Object> addSchedule(

@Operation(summary = "Delete a Kraftwerk execution schedule(s) by its survey name")
@DeleteMapping(path = "/delete")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Object> deleteSchedule(
@Parameter(description = "Survey name of the schedule(s) to delete") @RequestParam("surveyName") String surveyName
){
Expand All @@ -122,6 +126,7 @@ public ResponseEntity<Object> deleteSchedule(

@Operation(summary = "Set last execution date with new date or empty")
@PostMapping(path = "/setLastExecutionDate")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Object> setSurveyLastExecution(
@Parameter(description = "Survey name to call Kraftwerk on") @RequestBody String surveyName,
@Parameter(description = "Date to save as last execution date", example = "2024-01-01T12:00:00") @RequestParam("newDate") LocalDateTime newDate
Expand All @@ -138,6 +143,7 @@ public ResponseEntity<Object> setSurveyLastExecution(

@Operation(summary = "Delete expired schedules")
@DeleteMapping(path = "/delete/expired-schedules")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Object> deleteExpiredSchedules() throws NotFoundException, IOException {
Set<String> storedSurveySchedulesNames = new HashSet<>();
for(ScheduleModel scheduleModel : scheduleApiPort.getAllSchedules()){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -35,6 +36,7 @@ public UtilsController(SurveyUnitApiPort surveyUnitService,VolumetryLogService v

@Operation(summary = "Split a XML file into smaller ones")
@PutMapping(path = "/utils/split/lunatic-xml")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Object> saveResponsesFromXmlFile(@RequestParam("inputFolder") String inputFolder,
@RequestParam("outputFolder") String outputFolder,
@RequestParam("filename") String filename,
Expand All @@ -46,6 +48,7 @@ public ResponseEntity<Object> saveResponsesFromXmlFile(@RequestParam("inputFolde

@Operation(summary = "Record volumetrics of each campaign in a folder")
@PutMapping(path = "/volumetrics/save-all-campaigns")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Object> saveVolumetry() throws IOException {
volumetryLogService.writeVolumetries(surveyUnitService);
volumetryLogService.cleanOldFiles();
Expand Down
Loading
Loading