Skip to content
Open
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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@
<artifactId>spring-cloud-stream-test-binder</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
Expand Down
106 changes: 106 additions & 0 deletions src/main/java/org/gridsuite/explore/server/UserAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.gridsuite.explore.server;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.*;
import java.util.stream.Collectors;

public class UserAuthentication implements Authentication {

private final String principal;

private final Collection<GrantedAuthority> authorities;

private boolean authenticated = false;

public UserAuthentication(String principal, Collection<? extends GrantedAuthority> authorities) {
this.principal = principal;
this.authorities = authorities == null
? AuthorityUtils.NO_AUTHORITIES
: Collections.unmodifiableList(new ArrayList<>(authorities));
setAuthenticated(true);
}

public UserAuthentication(String principal, String roles) {
List<GrantedAuthority> authorities = Collections.emptyList();
if (roles != null && !roles.isEmpty()) {
authorities = Arrays.stream(roles.split("\\|"))
.map(String::trim)
.filter(role -> !role.isEmpty())
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast)
.toList();
}
this(principal, authorities);
}

// TODO: pour les roles, on recoit un String ou une liste d'authorities ? on renvoie un String on une liste d'authorities ?

public String getUserId() {
return principal;
}

public String getRoles() {
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining("|"));
}

// methods needed by interface

@Override
public String getName() {
return principal;
}

@Override
public String getPrincipal() {
return this.principal;
}

@Override
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public boolean isAuthenticated() {
return this.authenticated;
}

@Override
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}

@Override
public Object getCredentials() {
return null;
}

@Override
public Object getDetails() {
return null;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append(" [");
sb.append("Principal=").append(getPrincipal()).append(", ");
sb.append("Authenticated=").append(isAuthenticated()).append(", ");
sb.append("Granted Authorities=").append(this.authorities);
sb.append("]");
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.explore.server;
package org.gridsuite.explore.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import jakarta.servlet.http.HttpServletRequest;
import org.gridsuite.explore.server.UserAuthentication;
import org.springframework.boot.jackson.JsonComponentModule;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
Expand All @@ -20,9 +20,8 @@
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.IOException;
import java.util.Collections;
Expand All @@ -43,39 +42,38 @@ public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
restTemplate.getMessageConverters().set(i, mappingJackson2HttpMessageConverter());
}

restTemplate.setInterceptors(
Collections.singletonList(new RoleHeaderForwardingInterceptor())
);
}

restTemplate.setInterceptors(
Collections.singletonList(new HeaderForwardingInterceptor())
);

return restTemplate;
}

/**
* In our microservice architecture, user permissions (roles) must be preserved when
* one service calls another. This interceptor automatically forwards the "roles" header
* In our microservice architecture, user permissions (userId and roles) must be preserved when
* one service calls another. This interceptor automatically forwards the "userId" and "roles" headers
* from incoming requests to any outgoing REST calls made with this RestTemplate.
*
* Without this interceptor, authorization information would be lost in service-to-service
* communication.
*/
public static class RoleHeaderForwardingInterceptor implements ClientHttpRequestInterceptor {
public static class HeaderForwardingInterceptor implements ClientHttpRequestInterceptor {

private static final String ROLES_HEADER = "roles";
private static final String USER_ID_HEADER = "userId";

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
UserAuthentication authentication = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication();

// If we have a current request, copy its roles header to the outgoing request
if (attributes != null) {
HttpServletRequest currentRequest = attributes.getRequest();
String roles = currentRequest.getHeader(ROLES_HEADER);
if (authentication != null) {
request.getHeaders().set(USER_ID_HEADER, authentication.getUserId());

if (roles != null && !roles.isEmpty()) {
String roles = authentication.getRoles();
if (!roles.isEmpty()) {
request.getHeaders().set(ROLES_HEADER, roles);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.gridsuite.explore.server.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.gridsuite.explore.server.UserAuthentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
* @author Caroline Jeandat <caroline.jeandat at rte-france.com>
*/
public class SecurityFilter extends OncePerRequestFilter {

private static final String HEADER_ROLES = "roles";
private static final String HEADER_USER_ID = "userId";

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

String userId = request.getHeader(HEADER_USER_ID);
String rolesHeader = request.getHeader(HEADER_ROLES);

if (userId != null && !userId.isEmpty()) {
SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(userId, rolesHeader));
/*
Set<String> roles = Collections.emptySet();
if (rolesHeader != null && !rolesHeader.isEmpty()) {
roles = Arrays.stream(rolesHeader.split("\\|"))
.map(String::trim)
.filter(role -> !role.isEmpty())
.collect(Collectors.toSet());
}
userContext.setUserId(userId);
userContext.setRoles(roles);
*/
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.gridsuite.explore.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextHolderFilter;

/**
* @author Caroline Jeandat <caroline.jeandat at rte-france.com>
*/
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll()
)
.addFilterAfter(new SecurityFilter(), SecurityContextHolderFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.gridsuite.explore.server.services.ContingencyListService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -32,18 +33,22 @@ public ActionsController(ContingencyListService contingencyListService) {
this.contingencyListService = contingencyListService;
}

// TODO: on récupère l'élément depuis actions-server, donc vérifier qu'il existe bien dans directory-server
@GetMapping(value = "/identifier-contingency-lists/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get identifier contingency list by id from actions-server")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The identifier contingency list"),
@ApiResponse(responseCode = "404", description = "The identifier contingency list does not exists")})
@PreAuthorize("@authorizationService.canRead(#id)")
public ResponseEntity<String> getIdentifierContingencyList(@PathVariable("id") UUID id) {
return ResponseEntity.ok(contingencyListService.getIdentifierContingencyList(id));
}

// TODO: on récupère l'élément depuis actions-server, donc vérifier qu'il existe bien dans directory-server
@GetMapping(value = "/filters-contingency-lists/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Get filter based contingency list by id from actions-server")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The filter based contingency list"),
@ApiResponse(responseCode = "404", description = "The filter based contingency list does not exists")})
@PreAuthorize("@authorizationService.canRead(#id)")
public ResponseEntity<String> getFilterBasedContingencyList(@PathVariable("id") UUID id) {
return ResponseEntity.ok(contingencyListService.getFilterBasedContingencyList(id));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,27 @@ public CaseController(CaseService caseService) {
this.caseService = caseService;
}

// TODO: rien à checker
@PostMapping(value = "/cases", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<UUID> importCase(@RequestPart("file") MultipartFile file,
@RequestParam(value = "withExpiration", required = false, defaultValue = "false") boolean withExpiration) {
return ResponseEntity.ok(caseService.importCaseWithoutDirectoryElementCreation(file, withExpiration));
}

// TODO: appel à case-server et pas à directory-server -> rien à checker ?? vérifier l'utilité de cet endpoint
@DeleteMapping(value = "/cases/{caseUuid}")
public ResponseEntity<Void> deleteCase(@PathVariable("caseUuid") UUID caseUuid) {
caseService.deleteCase(caseUuid);
return ResponseEntity.ok().build();
}

// TODO: appel à case-server et pas à directory-server -> rien à checker ?? vérifier l'utilité de cet endpoint
@GetMapping(value = "/cases/{caseUuid}")
public ResponseEntity<Resource> downloadCase(@PathVariable("caseUuid") UUID caseUuid) {
return caseService.downloadCase(caseUuid);
}

// TODO: rien à checker
@GetMapping(value = "/cases/caseBaseName")
public ResponseEntity<String> getBaseName(@RequestParam("caseName") String caseName) {
return ResponseEntity.ok(caseService.getBaseName(caseName));
Expand Down
Loading
Loading