diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/batch/KraftwerkBatch.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/batch/KraftwerkBatch.java index 883eaf01..762b844b 100644 --- a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/batch/KraftwerkBatch.java +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/batch/KraftwerkBatch.java @@ -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; @@ -20,6 +22,7 @@ public class KraftwerkBatch implements CommandLineRunner { ConfigProperties configProperties; MinioConfig minioConfig; + FileUtilsInterface fileSystem; MinioClient minioClient; @Value("${fr.insee.postcollecte.files}") @@ -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()); } } @@ -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; @@ -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); } @@ -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])); diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/client/GenesisClient.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/client/GenesisClient.java index 2d12593d..4fb736bd 100644 --- a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/client/GenesisClient.java +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/client/GenesisClient.java @@ -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; @@ -28,28 +32,48 @@ 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 response = restTemplate.exchange(url, HttpMethod.GET, null, String.class); return response.getBody() != null ? response.getBody() : null; } public List getSurveyUnitIds(String idQuestionnaire) { String url = String.format("%s/idUEs/by-questionnaire?idQuestionnaire=%s", configProperties.getGenesisUrl(), idQuestionnaire); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, null, SurveyUnitId[].class); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(null, getHttpHeaders()), + SurveyUnitId[].class + ); return response.getBody() != null ? Arrays.asList(response.getBody()) : null; } public List getModes(String idCampaign) { String url = String.format("%s/modes/by-campaign?idCampaign=%s", configProperties.getGenesisUrl(), idCampaign); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, null, String[].class); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(null, getHttpHeaders()), + String[].class + ); List modes = new ArrayList<>(); if (response.getBody() != null) Arrays.asList(response.getBody()).forEach(modeLabel -> modes.add(Mode.getEnumFromModeName(modeLabel))); return modes; @@ -57,21 +81,47 @@ public List getModes(String idCampaign) { public List 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 response = restTemplate.exchange(url, HttpMethod.GET, null, SurveyUnitUpdateLatest[].class); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(null, getHttpHeaders()), + SurveyUnitUpdateLatest[].class + ); return response.getBody() != null ? Arrays.asList(response.getBody()) : null; } public List getUEsLatestState(String idQuestionnaire, List idUEs) { String url = String.format("%s/responses/simplified/by-list-ue-and-questionnaire/latest?idQuestionnaire=%s", configProperties.getGenesisUrl(), idQuestionnaire); - HttpEntity> request = new HttpEntity<>(idUEs); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, SurveyUnitUpdateLatest[].class); + HttpEntity> request = new HttpEntity<>(idUEs, getHttpHeaders()); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + request, + SurveyUnitUpdateLatest[].class + ); return response.getBody() != null ? Arrays.asList(response.getBody()) : null; } public List getQuestionnaireModelIds(String idCampaign) throws JsonProcessingException { String url = String.format("%s/questionnaires/by-campaign?idCampaign=%s", configProperties.getGenesisUrl(), idCampaign); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, null, String.class); + ResponseEntity 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(); + } } diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/ConfigProperties.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/ConfigProperties.java index 9076faa4..acc43c43 100644 --- a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/ConfigProperties.java +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/ConfigProperties.java @@ -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; } diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/OpenApiConfiguration.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/OpenApiConfiguration.java index 31f5b7f3..93cc43f1 100644 --- a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/OpenApiConfiguration.java +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/OpenApiConfiguration.java @@ -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; @@ -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() @@ -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); + } } diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/security/NoAuthConfiguration.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/security/NoAuthConfiguration.java new file mode 100644 index 00000000..7bb804e0 --- /dev/null +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/security/NoAuthConfiguration.java @@ -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(); + } +} diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/security/OIDCAuthConfiguration.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/security/OIDCAuthConfiguration.java new file mode 100644 index 00000000..72a5d47f --- /dev/null +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/configuration/security/OIDCAuthConfiguration.java @@ -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(); + } +} diff --git a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/process/MainProcessingGenesis.java b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/process/MainProcessingGenesis.java index 2aca12be..7c8df8e1 100644 --- a/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/process/MainProcessingGenesis.java +++ b/kraftwerk-api/src/main/java/fr/insee/kraftwerk/api/process/MainProcessingGenesis.java @@ -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; } diff --git a/kraftwerk-api/src/main/resources/application.properties b/kraftwerk-api/src/main/resources/application.properties index cc36d2ee..609a1a3b 100644 --- a/kraftwerk-api/src/main/resources/application.properties +++ b/kraftwerk-api/src/main/resources/application.properties @@ -14,6 +14,7 @@ fr.insee.kraftwerk.version=@project.version@ 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 =" @@ -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/** \ No newline at end of file diff --git a/kraftwerk-api/src/main/resources/kraftwerk_example.properties b/kraftwerk-api/src/main/resources/kraftwerk_example.properties index 3b5d3071..9f228948 100644 --- a/kraftwerk-api/src/main/resources/kraftwerk_example.properties +++ b/kraftwerk-api/src/main/resources/kraftwerk_example.properties @@ -2,6 +2,7 @@ ## Properties that are fixed by OPS ## ## For local usage, create kraftwerk.properties ## ################################################## +fr.insee.kraftwerk.application.host.url=***:${server.port} # Folders for in and out fr.insee.postcollecte.files = *** @@ -13,4 +14,9 @@ logging.file.name= ***\\kraftwerk.log logging.pattern.rolling-file-name= ***\\kraftwerk-%d{yyyy-MM-dd}.%i.log # Genesis API -fr.insee.postcollecte.genesis.api.url=*** \ No newline at end of file +fr.insee.postcollecte.genesis.api.url=*** + +# Auth +fr.insee.kraftwerk.oidc.auth-server-url=*** +fr.insee.kraftwerk.oidc.realm=*** +springdoc.swagger-ui.oauth.client-id=*** \ No newline at end of file diff --git a/pom.xml b/pom.xml index 694edebd..0e027877 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,15 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + +