diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 00000000..dba11795 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,28 @@ +name: Trivy Analysis + +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: 'temurin' + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + format: 'table' + scan-type: 'repo' + exit-code: '1' + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..dd3738a2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v3.6.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test] + - repo: local + hooks: + - id: trivy-scan + name: Trivy scan + entry: trivy fs . --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 + language: system + pass_filenames: false \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6933d3e0..8aa7b8b4 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ Modules for queen back-office - 4.3.19 + 4.3.22 21 21 @@ -30,7 +30,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.4 + 3.3.6 diff --git a/queen-application/Dockerfile b/queen-application/Dockerfile index 14bf3335..addc9f92 100644 --- a/queen-application/Dockerfile +++ b/queen-application/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:21.0.4_7-jre-alpine +FROM eclipse-temurin:21.0.5_11-jre-alpine ENV PATH_TO_JAR=/opt/app/app.jar WORKDIR /opt/app/ diff --git a/queen-application/pom.xml b/queen-application/pom.xml index da10d019..9a28111f 100644 --- a/queen-application/pom.xml +++ b/queen-application/pom.xml @@ -19,7 +19,7 @@ 2.17.0 20240303 33.3.1-jre - 1.5.2 + 1.5.3 diff --git a/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthConstants.java b/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthConstants.java deleted file mode 100644 index 5fe63ad2..00000000 --- a/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthConstants.java +++ /dev/null @@ -1,9 +0,0 @@ -package fr.insee.queen.application.configuration.auth; - -public class AuthConstants { - private AuthConstants() { - throw new IllegalStateException("Constants class"); - } - - public static final String ROLE_PREFIX = "ROLE_"; -} diff --git a/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthorityRoleEnum.java b/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthorityRoleEnum.java index adcbca98..998c4bf5 100644 --- a/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthorityRoleEnum.java +++ b/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/AuthorityRoleEnum.java @@ -6,5 +6,11 @@ public enum AuthorityRoleEnum { REVIEWER, REVIEWER_ALTERNATIVE, INTERVIEWER, - SURVEY_UNIT + SURVEY_UNIT; + + public static final String ROLE_PREFIX = "ROLE_"; + + public String securityRole() { + return ROLE_PREFIX + this.name(); + } } diff --git a/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverter.java b/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverter.java index c766521e..229ac0a0 100644 --- a/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverter.java +++ b/queen-application/src/main/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverter.java @@ -14,52 +14,58 @@ @AllArgsConstructor public class GrantedAuthorityConverter implements Converter> { + public static final String REALM_ACCESS_ROLE = "roles"; public static final String REALM_ACCESS = "realm_access"; - public static final String ROLES = "roles"; + private final Map> roles; private final OidcProperties oidcProperties; - private final RoleProperties roleProperties; + public GrantedAuthorityConverter(OidcProperties oidcProperties, RoleProperties roleProperties) { + this.roles = new HashMap<>(); + this.oidcProperties = oidcProperties; + initRole(roleProperties.surveyUnit(), AuthorityRoleEnum.SURVEY_UNIT); + initRole(roleProperties.interviewer(), AuthorityRoleEnum.INTERVIEWER); + initRole(roleProperties.reviewer(), AuthorityRoleEnum.REVIEWER); + initRole(roleProperties.reviewerAlternative(), AuthorityRoleEnum.REVIEWER_ALTERNATIVE); + initRole(roleProperties.admin(), AuthorityRoleEnum.ADMIN); + initRole(roleProperties.webclient(), AuthorityRoleEnum.WEBCLIENT); + } + + @SuppressWarnings("unchecked") @Override public Collection convert(@NonNull Jwt jwt) { - List roles = getRoles(jwt); + List userRoles = getUserRoles(jwt); - return roles.stream() - .map(role -> { - if(role == null || role.isEmpty()) { - return null; - } - if (role.equals(roleProperties.surveyUnit())) { - return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.SURVEY_UNIT); - } - if (role.equals(roleProperties.reviewer())) { - return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.REVIEWER); - } - if (role.equals(roleProperties.reviewerAlternative())) { - return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.REVIEWER_ALTERNATIVE); - } - if (role.equals(roleProperties.interviewer())) { - return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.INTERVIEWER); - } - if (role.equals(roleProperties.admin())) { - return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.ADMIN); - } - if (role.equals(roleProperties.webclient())) { - return new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.WEBCLIENT); - } - return null; - }) - .filter(Objects::nonNull) + return userRoles.stream() + .filter(this.roles::containsKey) + .map(this.roles::get) + .flatMap(List::stream) + .distinct() .collect(Collectors.toCollection(ArrayList::new)); } + private void initRole(String configRole, AuthorityRoleEnum authorityRole) { + // config role is not set + if(configRole == null || configRole.isBlank()) { + return; + } + + this.roles.compute(configRole, (key, grantedAuthorities) -> { + if(grantedAuthorities == null) { + grantedAuthorities = new ArrayList<>(); + } + grantedAuthorities.add(new SimpleGrantedAuthority(authorityRole.securityRole())); + return grantedAuthorities; + }); + } + @SuppressWarnings("unchecked") - private List getRoles(Jwt jwt) { + private List getUserRoles(Jwt jwt) { Map claims = jwt.getClaims(); if(oidcProperties.roleClaim().isEmpty()) { Map realmAccess = jwt.getClaim(REALM_ACCESS); - return (List) realmAccess.get(ROLES); + return (List) realmAccess.get(REALM_ACCESS_ROLE); } return (List) claims.get(oidcProperties.roleClaim()); } diff --git a/queen-application/src/main/java/fr/insee/queen/application/configuration/log/LogInterceptor.java b/queen-application/src/main/java/fr/insee/queen/application/configuration/log/LogInterceptor.java index fb979710..7d0a4364 100644 --- a/queen-application/src/main/java/fr/insee/queen/application/configuration/log/LogInterceptor.java +++ b/queen-application/src/main/java/fr/insee/queen/application/configuration/log/LogInterceptor.java @@ -28,7 +28,7 @@ public boolean preHandle(HttpServletRequest request, @Nonnull HttpServletRespons Authentication authentication = authenticationHelper.getAuthenticationPrincipal(); - String userId = authentication.getName(); + String userId = authentication.getName().toUpperCase(); MDC.put("id", fishTag); MDC.put("path", operationPath); diff --git a/queen-application/src/main/resources/logback-spring.xml b/queen-application/src/main/resources/logback-spring.xml index 3f3b11da..b2c7a81a 100644 --- a/queen-application/src/main/resources/logback-spring.xml +++ b/queen-application/src/main/resources/logback-spring.xml @@ -9,7 +9,7 @@ ${LOG_FILE} - ${LOG_FILE}.%d{yyyy-MM-dd}.gz + ${LOG_FILE}.%d{yyyy-MM-dd}.log.gz ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY} diff --git a/queen-application/src/test/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverterTest.java b/queen-application/src/test/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverterTest.java index f0e39f61..2f32fafa 100644 --- a/queen-application/src/test/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverterTest.java +++ b/queen-application/src/test/java/fr/insee/queen/application/configuration/auth/GrantedAuthorityConverterTest.java @@ -24,8 +24,6 @@ class GrantedAuthorityConverterTest { private OidcProperties oidcProperties; - private Map jwtHeaders; - private static final String JWT_ROLE_INTERVIEWER = "interviewer"; private static final String JWT_ROLE_REVIEWER = "reviewer"; private static final String JWT_ROLE_REVIEWER_ALTERNATIVE = "reviewerAlternative"; @@ -35,9 +33,8 @@ class GrantedAuthorityConverterTest { @BeforeEach void init() { - oidcProperties = new OidcProperties(true, "host", "url", "realm", "principal-attribute", "roleClaim", "client-id"); - jwtHeaders = new HashMap<>(); - jwtHeaders.put("header", "headerValue"); + oidcProperties = new OidcProperties(true, "host", "url", "realm", "principal-attribute", "", "client-id"); + } @Test @@ -45,14 +42,11 @@ void init() { void testConverter01() { RoleProperties roleProperties = new RoleProperties("", null, JWT_ROLE_ADMIN, JWT_ROLE_WEBCLIENT, JWT_ROLE_REVIEWER_ALTERNATIVE, JWT_ROLE_SURVEY_UNIT); converter = new GrantedAuthorityConverter(oidcProperties, roleProperties); - Map claims = new HashMap<>(); List tokenRoles = new ArrayList<>(); tokenRoles.add(null); tokenRoles.add(""); - claims.put(oidcProperties.roleClaim(), tokenRoles); - - Jwt jwt = new Jwt("user-id", Instant.now(), Instant.MAX, jwtHeaders, claims); + Jwt jwt = createJwt(tokenRoles); Collection authorities = converter.convert(jwt); assertThat(authorities).isEmpty(); } @@ -62,55 +56,74 @@ void testConverter01() { void testConverter02() { RoleProperties roleProperties = new RoleProperties(JWT_ROLE_INTERVIEWER, JWT_ROLE_REVIEWER, JWT_ROLE_ADMIN, JWT_ROLE_WEBCLIENT, JWT_ROLE_REVIEWER_ALTERNATIVE, JWT_ROLE_SURVEY_UNIT); converter = new GrantedAuthorityConverter(oidcProperties, roleProperties); - Map claims = new HashMap<>(); List tokenRoles = List.of("dummyRole1", roleProperties.reviewer(), "dummyRole2", roleProperties.interviewer(), "dummyRole3", roleProperties.surveyUnit()); - claims.put(oidcProperties.roleClaim(), tokenRoles); - Jwt jwt = new Jwt("user-id", Instant.now(), Instant.MAX, jwtHeaders, claims); + Jwt jwt = createJwt(tokenRoles); Collection authorities = converter.convert(jwt); assertThat(authorities) .hasSize(3) .containsExactlyInAnyOrder( - new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.INTERVIEWER), - new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.SURVEY_UNIT), - new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.REVIEWER)); + new SimpleGrantedAuthority(AuthorityRoleEnum.INTERVIEWER.securityRole()), + new SimpleGrantedAuthority(AuthorityRoleEnum.SURVEY_UNIT.securityRole()), + new SimpleGrantedAuthority(AuthorityRoleEnum.REVIEWER.securityRole())); + } + + @Test + @DisplayName("Given a JWT, when converting roles, then accept a config role can be used for multiple app roles") + void testConverter03() { + String dummyRole = "dummyRole"; + String dummyRole2 = "dummyRole2"; + RoleProperties roleProperties = new RoleProperties(dummyRole, dummyRole, dummyRole2, dummyRole2, null, dummyRole2); + oidcProperties = new OidcProperties(true, "host", "url", "realm", "principal-attribute", "", "client-id"); + converter = new GrantedAuthorityConverter(oidcProperties, roleProperties); + + List tokenRoles = List.of(dummyRole, "role-not-used", dummyRole2, "role-not-used-2"); + Jwt jwt = createJwt(tokenRoles); + + Collection authorities = converter.convert(jwt); + assertThat(authorities) + .hasSize(5) + .contains( + new SimpleGrantedAuthority(AuthorityRoleEnum.INTERVIEWER.securityRole()), + new SimpleGrantedAuthority(AuthorityRoleEnum.REVIEWER.securityRole()), + new SimpleGrantedAuthority(AuthorityRoleEnum.ADMIN.securityRole()), + new SimpleGrantedAuthority(AuthorityRoleEnum.WEBCLIENT.securityRole()), + new SimpleGrantedAuthority(AuthorityRoleEnum.SURVEY_UNIT.securityRole())); } @ParameterizedTest @MethodSource("provideJWTRoleWithAppRoleAssociated") @DisplayName("Given a JWT, when converting roles, then assure each JWT role is converted to equivalent app role") - void testConverter03(String jwtRole, AuthorityRoleEnum appRole) { + void testConverter04(String jwtRole, AuthorityRoleEnum appRole) { RoleProperties roleProperties = new RoleProperties(JWT_ROLE_INTERVIEWER, JWT_ROLE_REVIEWER, JWT_ROLE_ADMIN, JWT_ROLE_WEBCLIENT, JWT_ROLE_REVIEWER_ALTERNATIVE, JWT_ROLE_SURVEY_UNIT); converter = new GrantedAuthorityConverter(oidcProperties, roleProperties); - Map claims = new HashMap<>(); List tokenRoles = List.of(jwtRole); - claims.put(oidcProperties.roleClaim(), tokenRoles); - Jwt jwt = new Jwt("user-id", Instant.now(), Instant.MAX, jwtHeaders, claims); + Jwt jwt = createJwt(tokenRoles); Collection authorities = converter.convert(jwt); assertThat(authorities) .hasSize(1) - .contains(new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + appRole)); + .contains(new SimpleGrantedAuthority(appRole.securityRole())); } @Test - @DisplayName("Given a JWT, when no role claim is defined, then default role claim is used") - void testConverter04() { - oidcProperties = new OidcProperties(true, "host", "url", "realm", "principal-attribute", "", "client-id"); + @DisplayName("Given a JWT, when role claim is defined, then role claim is used to retrieve roles") + void testConverter05() { + oidcProperties = new OidcProperties(true, "host", "url", "realm", "principal-attribute", "roleClaim", "client-id"); RoleProperties roleProperties = new RoleProperties(JWT_ROLE_INTERVIEWER, JWT_ROLE_REVIEWER, JWT_ROLE_ADMIN, JWT_ROLE_WEBCLIENT, JWT_ROLE_REVIEWER_ALTERNATIVE, JWT_ROLE_SURVEY_UNIT); converter = new GrantedAuthorityConverter(oidcProperties, roleProperties); Map claims = new HashMap<>(); - Map roleClaims = new HashMap<>(); List tokenRoles = List.of(JWT_ROLE_INTERVIEWER, JWT_ROLE_REVIEWER); - roleClaims.put(GrantedAuthorityConverter.ROLES, tokenRoles); - claims.put(GrantedAuthorityConverter.REALM_ACCESS, roleClaims); + claims.put(oidcProperties.roleClaim(), tokenRoles); + Map jwtHeaders = new HashMap<>(); + jwtHeaders.put("header", "headerValue"); Jwt jwt = new Jwt("user-id", Instant.now(), Instant.MAX, jwtHeaders, claims); Collection authorities = converter.convert(jwt); assertThat(authorities) .hasSize(2) - .contains(new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.INTERVIEWER)) - .contains(new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + AuthorityRoleEnum.REVIEWER)); + .contains(new SimpleGrantedAuthority(AuthorityRoleEnum.INTERVIEWER.securityRole())) + .contains(new SimpleGrantedAuthority(AuthorityRoleEnum.REVIEWER.securityRole())); } private static Stream provideJWTRoleWithAppRoleAssociated() { @@ -122,4 +135,16 @@ private static Stream provideJWTRoleWithAppRoleAssociated() { Arguments.of(JWT_ROLE_WEBCLIENT, AuthorityRoleEnum.WEBCLIENT), Arguments.of(JWT_ROLE_SURVEY_UNIT, AuthorityRoleEnum.SURVEY_UNIT)); } + + private Jwt createJwt(List tokenRoles) { + Map jwtHeaders = new HashMap<>(); + jwtHeaders.put("header", "headerValue"); + + Map claims = new HashMap<>(); + Map> realmRoles = new HashMap<>(); + realmRoles.put(GrantedAuthorityConverter.REALM_ACCESS_ROLE, tokenRoles); + claims.put(GrantedAuthorityConverter.REALM_ACCESS, realmRoles); + + return new Jwt("user-id", Instant.now(), Instant.MAX, jwtHeaders, claims); + } } diff --git a/queen-application/src/test/java/fr/insee/queen/application/utils/AuthenticatedUserTestHelper.java b/queen-application/src/test/java/fr/insee/queen/application/utils/AuthenticatedUserTestHelper.java index 41a20703..f7ccf73e 100644 --- a/queen-application/src/test/java/fr/insee/queen/application/utils/AuthenticatedUserTestHelper.java +++ b/queen-application/src/test/java/fr/insee/queen/application/utils/AuthenticatedUserTestHelper.java @@ -1,6 +1,5 @@ package fr.insee.queen.application.utils; -import fr.insee.queen.application.configuration.auth.AuthConstants; import fr.insee.queen.application.configuration.auth.AuthorityRoleEnum; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -51,7 +50,7 @@ public JwtAuthenticationToken getSurveyUnitUser() { public JwtAuthenticationToken getAuthenticatedUser(AuthorityRoleEnum... roles) { List authorities = new ArrayList<>(); for (AuthorityRoleEnum role : roles) { - authorities.add(new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + role.name())); + authorities.add(new SimpleGrantedAuthority(role.securityRole())); } Map headers = Map.of("typ", "JWT"); @@ -63,7 +62,7 @@ public JwtAuthenticationToken getAuthenticatedUser(AuthorityRoleEnum... roles) { public AnonymousAuthenticationToken getNotAuthenticatedUser() { Map principal = new HashMap<>(); - AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken("id", principal, List.of(new SimpleGrantedAuthority(AuthConstants.ROLE_PREFIX + "ANONYMOUS"))); + AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken("id", principal, List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))); auth.setAuthenticated(false); return auth; } diff --git a/queen-infra-depositproof/pom.xml b/queen-infra-depositproof/pom.xml index 27dd82ce..f1c9a3ba 100644 --- a/queen-infra-depositproof/pom.xml +++ b/queen-infra-depositproof/pom.xml @@ -42,6 +42,10 @@ commons-logging commons-logging + + commons-io + commons-io +