Skip to content

Commit c8347b3

Browse files
committed
Merge branch 'devHabilitation' into develop
2 parents 5b14413 + 2be08f9 commit c8347b3

File tree

13 files changed

+539
-28
lines changed

13 files changed

+539
-28
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fr.insee.genesis.configuration.auth.security;
2+
3+
public enum ApplicationRole {
4+
ADMIN,
5+
USER_KRAFTWERK,
6+
USER_PLATINE,
7+
COLLECT_PLATFORM,
8+
READER
9+
}
10+
Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,120 @@
11
package fr.insee.genesis.configuration.auth.security;
22

3-
import fr.insee.genesis.configuration.Config;
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.Setter;
46
import lombok.extern.slf4j.Slf4j;
5-
import org.springframework.beans.factory.annotation.Autowired;
67
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
8+
import org.springframework.boot.context.properties.ConfigurationProperties;
79
import org.springframework.context.annotation.Bean;
810
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.core.convert.converter.Converter;
12+
import org.springframework.http.HttpMethod;
913
import org.springframework.security.config.Customizer;
14+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
1015
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1116
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1217
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
1318
import org.springframework.security.config.http.SessionCreationPolicy;
19+
import org.springframework.security.core.GrantedAuthority;
20+
import org.springframework.security.oauth2.jwt.Jwt;
21+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
1422
import org.springframework.security.web.SecurityFilterChain;
23+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
1524
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
1625

26+
import java.util.Collection;
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.Map;
30+
1731
@Configuration
1832
@EnableWebSecurity
33+
@EnableMethodSecurity(prePostEnabled = true)
1934
@Slf4j
2035
@ConditionalOnProperty(name = "fr.insee.genesis.authentication", havingValue = "OIDC")
36+
@ConfigurationProperties(prefix = "fr.insee.genesis.security")
37+
@RequiredArgsConstructor
2138
public class OIDCSecurityConfig {
2239

23-
Config config;
24-
@Autowired
25-
public OIDCSecurityConfig(Config config) {
26-
this.config = config;
27-
}
40+
@Getter
41+
@Setter
42+
private String[] whitelistMatchers;
43+
private static final String ROLE_PREFIX = "ROLE_";
44+
private final RoleConfiguration roleConfiguration;
45+
private final SecurityTokenProperties inseeSecurityTokenProperties;
2846

29-
@Bean
30-
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
31-
http
32-
.csrf(AbstractHttpConfigurer::disable)
33-
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
34-
for (var pattern : config.getWhiteList()) {
35-
http.authorizeHttpRequests(authorize ->
36-
authorize
37-
.requestMatchers(AntPathRequestMatcher.antMatcher(pattern)).permitAll()
38-
);
39-
}
40-
http
41-
.authorizeHttpRequests(configurer -> configurer
42-
.anyRequest().authenticated()
43-
)
44-
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
45-
return http.build();
47+
@Bean
48+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
49+
http
50+
.csrf(AbstractHttpConfigurer::disable)
51+
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
52+
for (var pattern : whitelistMatchers) {
53+
http.authorizeHttpRequests(authorize ->
54+
authorize
55+
.requestMatchers(AntPathRequestMatcher.antMatcher(pattern)).permitAll()
56+
);
4657
}
58+
http
59+
.authorizeHttpRequests(configurer -> configurer
60+
.requestMatchers(HttpMethod.GET,"/questionnaires/**").hasRole(String.valueOf(ApplicationRole.READER))
61+
.requestMatchers(HttpMethod.GET,"/modes/**").hasRole(String.valueOf(ApplicationRole.READER))
62+
.requestMatchers(HttpMethod.GET,"/interrogations/**").hasRole(String.valueOf(ApplicationRole.READER))
63+
.requestMatchers(HttpMethod.GET,"/campaigns/**").hasRole(String.valueOf(ApplicationRole.READER))
64+
.anyRequest().authenticated()
65+
)
66+
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
67+
return http.build();
68+
}
69+
70+
@Bean
71+
JwtAuthenticationConverter jwtAuthenticationConverter() {
72+
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
73+
jwtAuthenticationConverter.setPrincipalClaimName(inseeSecurityTokenProperties.getOidcClaimUsername());
74+
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
75+
return jwtAuthenticationConverter;
76+
}
4777

78+
79+
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
80+
return new Converter<Jwt, Collection<GrantedAuthority>>() {
81+
@Override
82+
@SuppressWarnings({"unchecked"})
83+
public Collection<GrantedAuthority> convert(Jwt source) {
84+
85+
String[] claimPath = inseeSecurityTokenProperties.getOidcClaimRole().split("\\.");
86+
Map<String, Object> claims = source.getClaims();
87+
try {
88+
for (int i = 0; i < claimPath.length - 1; i++) {
89+
claims = (Map<String, Object>) claims.get(claimPath[i]);
90+
}
91+
if (claims != null) {
92+
List<String> tokenClaims = (List<String>) claims.getOrDefault(claimPath[claimPath.length - 1], List.of());
93+
// Collect distinct values from mapping associated with input keys
94+
List<String> claimedRoles = tokenClaims.stream()
95+
.filter(roleConfiguration.getRolesByClaim()::containsKey) // Ensure the key exists in the mapping
96+
.flatMap(key -> roleConfiguration.getRolesByClaim().get(key).stream()) // Get the list of values associated with the key
97+
.distinct() // Remove duplicates
98+
.toList();
99+
100+
return Collections.unmodifiableCollection(claimedRoles.stream().map(s -> new GrantedAuthority() {
101+
@Override
102+
public String getAuthority() {
103+
return ROLE_PREFIX + s;
104+
}
105+
106+
@Override
107+
public String toString() {
108+
return getAuthority();
109+
}
110+
}).toList());
111+
}
112+
} catch (ClassCastException e) {
113+
// role path not correctly found, assume that no role for this user
114+
return List.of();
115+
}
116+
return List.of();
117+
}
118+
};
119+
}
48120
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package fr.insee.genesis.configuration.auth.security;
2+
3+
import jakarta.annotation.PostConstruct;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
9+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
10+
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
11+
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
12+
13+
import java.util.ArrayList;
14+
import java.util.HashMap;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
@Configuration
19+
@Slf4j
20+
public class RoleConfiguration {
21+
22+
//Mapping des claims du jeton sur les roles applicatifs
23+
@Value("#{'${app.role.admin.claims}'.split(',')}")
24+
private List<String> adminClaims;
25+
@Value("#{'${app.role.user-kraftwerk.claims}'.split(',')}")
26+
private List<String> userKraftwerkClaims;
27+
@Value("#{'${app.role.user-platine.claims}'.split(',')}")
28+
private List<String> userPlatineClaims;
29+
@Value("#{'${app.role.reader.claims}'.split(',')}")
30+
private List<String> readerClaims;
31+
@Value("#{'${app.role.collect-platform.claims}'.split(',')}")
32+
private List<String> collectPlatformClaims;
33+
34+
public Map<String, List<String>> getRolesByClaim() {
35+
return rolesByClaim;
36+
}
37+
38+
private Map<String, List<String>> rolesByClaim;
39+
40+
//Defines a role hierarchy
41+
//ADMIN implies USER role too
42+
//USER implies READER role too
43+
//so an admin has 2 roles: ADMIN/USER
44+
@Bean
45+
static RoleHierarchy roleHierarchy() {
46+
return RoleHierarchyImpl.withDefaultRolePrefix()
47+
.role(ApplicationRole.ADMIN.toString()).implies(ApplicationRole.USER_KRAFTWERK.toString())
48+
.role(ApplicationRole.ADMIN.toString()).implies(ApplicationRole.USER_PLATINE.toString())
49+
.role(ApplicationRole.ADMIN.toString()).implies(ApplicationRole.COLLECT_PLATFORM.toString())
50+
.role(ApplicationRole.USER_KRAFTWERK.toString()).implies(ApplicationRole.READER.toString())
51+
.role(ApplicationRole.USER_PLATINE.toString()).implies(ApplicationRole.READER.toString())
52+
.build();
53+
}
54+
55+
// and, if using pre-post method security also add
56+
@Bean
57+
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
58+
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
59+
expressionHandler.setRoleHierarchy(roleHierarchy);
60+
return expressionHandler;
61+
}
62+
63+
@PostConstruct
64+
public void initialization() {
65+
66+
rolesByClaim = new HashMap<>();
67+
68+
// Ajout des claims pour le rôle ADMIN
69+
adminClaims.forEach(claim -> rolesByClaim
70+
.computeIfAbsent(claim, k -> new ArrayList<>())
71+
.add(String.valueOf(ApplicationRole.ADMIN)));
72+
73+
// Ajout des claims pour le rôle USER_KRAFTWERK
74+
userKraftwerkClaims.forEach(claim -> rolesByClaim
75+
.computeIfAbsent(claim, k -> new ArrayList<>())
76+
.add(String.valueOf(ApplicationRole.USER_KRAFTWERK)));
77+
78+
// Ajout des claims pour le rôle USER_PLATINE
79+
userPlatineClaims.forEach(claim -> rolesByClaim
80+
.computeIfAbsent(claim, k -> new ArrayList<>())
81+
.add(String.valueOf(ApplicationRole.USER_PLATINE)));
82+
83+
// Ajout des claims pour le rôle COLLECT_PLATFORM
84+
collectPlatformClaims.forEach(claim -> rolesByClaim
85+
.computeIfAbsent(claim, k -> new ArrayList<>())
86+
.add(String.valueOf(ApplicationRole.COLLECT_PLATFORM)));
87+
88+
// Ajout des claims pour le rôle READER
89+
readerClaims.forEach(claim -> rolesByClaim
90+
.computeIfAbsent(claim, k -> new ArrayList<>())
91+
.add(String.valueOf(ApplicationRole.READER)));
92+
93+
94+
log.info("Roles configuration : {}", rolesByClaim);
95+
}
96+
97+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package fr.insee.genesis.configuration.auth.security;
2+
3+
import lombok.Data;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Configuration
8+
@ConfigurationProperties(
9+
prefix = "fr.insee.genesis.security.token"
10+
)
11+
@Data
12+
public class SecurityTokenProperties {
13+
14+
//Chemin pour récupérer la liste des rôles dans le jwt (token)
15+
private String oidcClaimRole;
16+
//Chemin pour récupérer le username dans le jwt (token)
17+
private String oidcClaimUsername;
18+
19+
}

src/main/java/fr/insee/genesis/controller/rest/ScheduleController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.swagger.v3.oas.annotations.Parameter;
1616
import lombok.extern.slf4j.Slf4j;
1717
import org.springframework.http.ResponseEntity;
18+
import org.springframework.security.access.prepost.PreAuthorize;
1819
import org.springframework.stereotype.Controller;
1920
import org.springframework.web.bind.annotation.DeleteMapping;
2021
import org.springframework.web.bind.annotation.GetMapping;
@@ -48,6 +49,7 @@ public ScheduleController(ScheduleApiPort scheduleApiPort, FileUtils fileUtils)
4849

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

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

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

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

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

139144
@Operation(summary = "Delete expired schedules")
140145
@DeleteMapping(path = "/delete/expired-schedules")
146+
@PreAuthorize("hasRole('ADMIN')")
141147
public ResponseEntity<Object> deleteExpiredSchedules() throws NotFoundException, IOException {
142148
Set<String> storedSurveySchedulesNames = new HashSet<>();
143149
for(ScheduleModel scheduleModel : scheduleApiPort.getAllSchedules()){

src/main/java/fr/insee/genesis/controller/rest/UtilsController.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.swagger.v3.oas.annotations.tags.Tag;
88
import lombok.extern.slf4j.Slf4j;
99
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.access.prepost.PreAuthorize;
1011
import org.springframework.stereotype.Controller;
1112
import org.springframework.web.bind.annotation.PutMapping;
1213
import org.springframework.web.bind.annotation.RequestMapping;
@@ -35,6 +36,7 @@ public UtilsController(SurveyUnitApiPort surveyUnitService,VolumetryLogService v
3536

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

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

0 commit comments

Comments
 (0)