Skip to content

Commit

Permalink
Merge pull request #196 from InseeFr/devAuth
Browse files Browse the repository at this point in the history
Add OIDC Authentication
  • Loading branch information
alexisszmundy authored Dec 13, 2024
2 parents 90552b7 + 32d443f commit 9218874
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import fr.insee.kraftwerk.api.process.MainProcessingGenesis;
import fr.insee.kraftwerk.api.services.KraftwerkService;
import fr.insee.kraftwerk.core.exceptions.KraftwerkException;
import fr.insee.kraftwerk.core.utils.files.FileSystemImpl;
import fr.insee.kraftwerk.core.utils.files.FileUtilsInterface;
import fr.insee.kraftwerk.core.utils.files.MinioImpl;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -20,6 +22,7 @@ public class KraftwerkBatch implements CommandLineRunner {

ConfigProperties configProperties;
MinioConfig minioConfig;
FileUtilsInterface fileSystem;
MinioClient minioClient;

@Value("${fr.insee.postcollecte.files}")
Expand All @@ -34,6 +37,9 @@ public KraftwerkBatch(ConfigProperties configProperties, MinioConfig minioConfig
this.minioConfig = minioConfig;
if(minioConfig.isEnable()){
minioClient = MinioClient.builder().endpoint(minioConfig.getEndpoint()).credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey()).build();
fileSystem = new MinioImpl(minioClient, minioConfig.getBucketName());
}else{
fileSystem = new FileSystemImpl(configProperties.getDefaultDirectory());
}
}

Expand All @@ -52,10 +58,12 @@ public void run(String... args) {
//1. Archive at end of execution (false or true)
//2. Integrate all reporting datas (false or true)
//3. Campaign name
//4. Authentication token for Genesis
KraftwerkServiceType kraftwerkServiceType = KraftwerkServiceType.valueOf(args[0]);
boolean archiveAtEnd = Boolean.parseBoolean(args[1]);
boolean withAllReportingData = Boolean.parseBoolean(args[2]);
String inDirectory = args[3];
String genesisToken = args[4];

//Kraftwerk service type related parameters
boolean fileByFile = kraftwerkServiceType == KraftwerkServiceType.FILE_BY_FILE;
Expand All @@ -65,22 +73,33 @@ public void run(String... args) {
}
if (kraftwerkServiceType == KraftwerkServiceType.GENESIS) {
archiveAtEnd = false;

}


//Run kraftwerk
if (kraftwerkServiceType == KraftwerkServiceType.GENESIS) {
MainProcessingGenesis mainProcessingGenesis = new MainProcessingGenesis(configProperties, new MinioImpl(minioClient, minioConfig.getBucketName()));
MainProcessingGenesis mainProcessingGenesis = new MainProcessingGenesis(
configProperties,
fileSystem,
genesisToken);
mainProcessingGenesis.runMain(inDirectory);
} else {
MainProcessing mainProcessing = new MainProcessing(inDirectory, fileByFile, withAllReportingData, withDDI, defaultDirectory, limitSize, new MinioImpl(minioClient, minioConfig.getBucketName()));
MainProcessing mainProcessing = new MainProcessing(
inDirectory,
fileByFile,
withAllReportingData,
withDDI,
defaultDirectory,
limitSize,
fileSystem);
mainProcessing.runMain();
}

//Archive
if (Boolean.TRUE.equals(archiveAtEnd)) {
KraftwerkService kraftwerkService = new KraftwerkService(configProperties, minioConfig);
kraftwerkService.archive(inDirectory, new MinioImpl(minioClient, minioConfig.getBucketName()));
kraftwerkService.archive(inDirectory, fileSystem);
}
System.exit(0);
}
Expand All @@ -101,8 +120,8 @@ public void run(String... args) {
* @throws IllegalArgumentException if invalid argument
*/
private static void checkArgs(String[] args) throws IllegalArgumentException{
if(args.length != 4) {
throw new IllegalArgumentException("Invalid number of arguments ! Got %s instead of 4 !".formatted(args.length));
if(args.length != 5) {
throw new IllegalArgumentException("Invalid number of arguments ! Got %s instead of 5 !".formatted(args.length));
}
if(!args[1].equals("true") && !args[1].equals("false")){
throw new IllegalArgumentException("Invalid archiveAtEnd boolean argument ! : %s".formatted(args[1]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

Expand All @@ -28,50 +32,96 @@ public class GenesisClient {
@Getter
private final ConfigProperties configProperties;

private final String authToken;


@Autowired
public GenesisClient(RestTemplateBuilder restTemplateBuilder, ConfigProperties configProperties) {
this.restTemplate = restTemplateBuilder.build();
this.configProperties = configProperties;
this.authToken = null;
}

public GenesisClient(RestTemplateBuilder restTemplateBuilder, ConfigProperties configProperties, String authToken) {
this.restTemplate = restTemplateBuilder.build();
this.configProperties = configProperties;
this.authToken = authToken;
}

public String pingGenesis(){
String url = String.format("%s/health-check", configProperties.getGenesisUrl());
//Null requestEntity because health check is whitelisted
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
return response.getBody() != null ? response.getBody() : null;
}

public List<SurveyUnitId> getSurveyUnitIds(String idQuestionnaire) {
String url = String.format("%s/idUEs/by-questionnaire?idQuestionnaire=%s", configProperties.getGenesisUrl(), idQuestionnaire);
ResponseEntity<SurveyUnitId[]> response = restTemplate.exchange(url, HttpMethod.GET, null, SurveyUnitId[].class);
ResponseEntity<SurveyUnitId[]> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(null, getHttpHeaders()),
SurveyUnitId[].class
);
return response.getBody() != null ? Arrays.asList(response.getBody()) : null;
}

public List<Mode> getModes(String idCampaign) {
String url = String.format("%s/modes/by-campaign?idCampaign=%s", configProperties.getGenesisUrl(), idCampaign);
ResponseEntity<String[]> response = restTemplate.exchange(url, HttpMethod.GET, null, String[].class);
ResponseEntity<String[]> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(null, getHttpHeaders()),
String[].class
);
List<Mode> modes = new ArrayList<>();
if (response.getBody() != null) Arrays.asList(response.getBody()).forEach(modeLabel -> modes.add(Mode.getEnumFromModeName(modeLabel)));
return modes;
}

public List<SurveyUnitUpdateLatest> getUELatestState(String idQuestionnaire, SurveyUnitId suId) {
String url = String.format("%s/responses/simplified/by-list-ue-and-questionnaire/latest?idQuestionnaire=%s&idUE=%s", configProperties.getGenesisUrl(), idQuestionnaire, suId.getIdUE());
ResponseEntity<SurveyUnitUpdateLatest[]> response = restTemplate.exchange(url, HttpMethod.GET, null, SurveyUnitUpdateLatest[].class);
ResponseEntity<SurveyUnitUpdateLatest[]> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(null, getHttpHeaders()),
SurveyUnitUpdateLatest[].class
);
return response.getBody() != null ? Arrays.asList(response.getBody()) : null;
}

public List<SurveyUnitUpdateLatest> getUEsLatestState(String idQuestionnaire, List<SurveyUnitId> idUEs) {
String url = String.format("%s/responses/simplified/by-list-ue-and-questionnaire/latest?idQuestionnaire=%s", configProperties.getGenesisUrl(), idQuestionnaire);
HttpEntity<List<SurveyUnitId>> request = new HttpEntity<>(idUEs);
ResponseEntity<SurveyUnitUpdateLatest[]> response = restTemplate.exchange(url, HttpMethod.POST, request, SurveyUnitUpdateLatest[].class);
HttpEntity<List<SurveyUnitId>> request = new HttpEntity<>(idUEs, getHttpHeaders());
ResponseEntity<SurveyUnitUpdateLatest[]> response = restTemplate.exchange(
url,
HttpMethod.POST,
request,
SurveyUnitUpdateLatest[].class
);
return response.getBody() != null ? Arrays.asList(response.getBody()) : null;
}

public List<String> getQuestionnaireModelIds(String idCampaign) throws JsonProcessingException {
String url = String.format("%s/questionnaires/by-campaign?idCampaign=%s", configProperties.getGenesisUrl(), idCampaign);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
ResponseEntity<String> response = restTemplate.exchange(url,
HttpMethod.GET,
new HttpEntity<>(null, getHttpHeaders()),
String.class);
ObjectMapper objectMapper = new ObjectMapper();
return response.getBody() != null ? objectMapper.readValue(response.getBody(), new TypeReference<>(){}) : null;
}

private HttpHeaders getHttpHeaders() {
//Auth
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " + (authToken == null ? getTokenValue() : authToken));
return httpHeaders;
}

private String getTokenValue() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
JwtAuthenticationToken oauthToken = (JwtAuthenticationToken) authentication;
return oauthToken.getToken().getTokenValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,16 @@ public class ConfigProperties {

@Value("${fr.insee.postcollecte.files}")
private String defaultDirectory;

//Auth
@Value("${fr.insee.kraftwerk.oidc.auth-server-url}")
private String authServerUrl;
@Value("${fr.insee.kraftwerk.oidc.realm}")
private String realm;
@Value("${fr.insee.kraftwerk.security.token.oidc-claim-role}")
private String oidcClaimRole;
@Value("${fr.insee.kraftwerk.security.token.oidc-claim-username}")
private String oidcClaimUsername;
@Value("#{'${fr.insee.kraftwerk.security.whitelist-matchers}'.split(',')}")
private String[] whiteList;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package fr.insee.kraftwerk.api.configuration;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;
import io.swagger.v3.oas.models.security.Scopes;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

Expand All @@ -12,7 +19,9 @@ public class OpenApiConfiguration {
@Value("${fr.insee.kraftwerk.version}")
private String projectVersion;

@Bean
public static final String BEARERSCHEME = "bearerAuth";
public static final String OAUTH2SCHEME = "oauth2";

public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
Expand All @@ -22,4 +31,45 @@ public OpenAPI customOpenAPI() {
);
}

@Bean
@ConditionalOnProperty(name = "fr.insee.kraftwerk.authentication", havingValue = "NONE")
public OpenAPI noAuthOpenAPI() {
return customOpenAPI();
}

@Bean
@ConditionalOnProperty(name = "fr.insee.kraftwerk.authentication", havingValue = "OIDC")
public OpenAPI oidcOpenAPI(ConfigProperties config) {
String authUrl = config.getAuthServerUrl() + "/realms/" + config.getRealm() + "/protocol/openid-connect";
return customOpenAPI()
.addSecurityItem(new SecurityRequirement().addList(OAUTH2SCHEME))
.addSecurityItem(new SecurityRequirement().addList(BEARERSCHEME))
.components(
new Components()
.addSecuritySchemes(OAUTH2SCHEME,
new SecurityScheme()
.name(OAUTH2SCHEME)
.type(SecurityScheme.Type.OAUTH2)
.flows(getFlows(authUrl))
)
.addSecuritySchemes(BEARERSCHEME,
new SecurityScheme()
.name(BEARERSCHEME)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
)
);
}

private OAuthFlows getFlows(String authUrl) {
OAuthFlows flows = new OAuthFlows();
OAuthFlow flow = new OAuthFlow();
Scopes scopes = new Scopes();
flow.setAuthorizationUrl(authUrl + "/auth");
flow.setTokenUrl(authUrl + "/token");
flow.setRefreshUrl(authUrl + "/token");
flow.setScopes(scopes);
return flows.authorizationCode(flow);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package fr.insee.kraftwerk.api.configuration.security;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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;

@ConditionalOnProperty(name = "fr.insee.kraftwerk.authentication", havingValue = "NONE")
@Configuration
@EnableWebSecurity
@Slf4j
@AllArgsConstructor
public class NoAuthConfiguration {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fr.insee.kraftwerk.api.configuration.security;

import fr.insee.kraftwerk.api.configuration.ConfigProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@Slf4j
@ConditionalOnProperty(name = "fr.insee.kraftwerk.authentication", havingValue = "OIDC")
public class OIDCAuthConfiguration {

ConfigProperties config;
@Autowired
public OIDCAuthConfiguration(ConfigProperties config) {
this.config = config;
}

@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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ public MainProcessingGenesis(ConfigProperties config, FileUtilsInterface fileUti
this.withDDI = withDDI;
}

public MainProcessingGenesis(ConfigProperties config, FileUtilsInterface fileUtilsInterface) {
this.client = new GenesisClient(new RestTemplateBuilder(), config);
public MainProcessingGenesis(ConfigProperties config, FileUtilsInterface fileUtilsInterface, String authToken) {
this.client = new GenesisClient(new RestTemplateBuilder(), config, authToken);
this.fileUtilsInterface = fileUtilsInterface;
this.withDDI = true;
}
Expand Down
8 changes: 8 additions & 0 deletions kraftwerk-api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ [email protected]@
springdoc.swagger-ui.path=/index.html
springdoc.api-docs.resolve-schema-properties=true
springdoc.swagger-ui.tagsSorter=alpha
springdoc.swagger-ui.oauth2RedirectUrl=${fr.insee.kraftwerk.application.host.url}/swagger-ui/oauth2-redirect.html


fr.insee.postcollecte.csv.output.quote ="
Expand Down Expand Up @@ -46,3 +47,10 @@ logging.pattern.rolling-file-name= C:\\Temp\\kraftwerk\\kraftwerk-%d{yyyy-MM-dd}

# Genesis API
fr.insee.postcollecte.genesis.api.url= http://api-reponses-enquetes.insee.fr/

#Auth
fr.insee.kraftwerk.authentication = OIDC
fr.insee.kraftwerk.security.token.oidc-claim-role=realm_access.roles
fr.insee.kraftwerk.security.token.oidc-claim-username=name
spring.security.oauth2.resourceserver.jwt.issuer-uri=${fr.insee.kraftwerk.oidc.auth-server-url}/realms/${fr.insee.kraftwerk.oidc.realm}
fr.insee.kraftwerk.security.whitelist-matchers=/v3/api-docs/**,/swagger-ui/**,/swagger-ui.html,/actuator/**,/error,/,/health-check/**
Loading

0 comments on commit 9218874

Please sign in to comment.