From 21324285ba184a12be331e2b9cd32b1eaf74256f Mon Sep 17 00:00:00 2001 From: Marco Massenzio <1153951+massenz@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:29:45 -0700 Subject: [PATCH] Upgrading Spring Boot to 1.1.3 (#55) * Upgraded Spring Boot to version 1.1.3 * Cleanup after updating Spring Boot * Fixed SonarLint smells --- gradle/wrapper/gradle-wrapper.properties | 2 +- jwt-opa/build.gradle | 20 +++++++------- .../jwt/ApiTokenAuthenticationFactory.java | 15 ++++++++--- .../OpaReactiveAuthorizationManager.java | 2 +- webapp-example/build.gradle | 16 +++++++++--- .../java/com/alertavert/opademo/DbInit.java | 15 ++++++----- .../alertavert/opademo/api/JwtController.java | 11 +++----- .../opademo/api/LoginController.java | 26 ++++++++----------- .../src/main/resources/application.yaml | 4 +-- .../opademo/api/LoginControllerTest.java | 25 +++++------------- .../data/ReactiveUsersRepositoryTest.java | 2 +- 11 files changed, 69 insertions(+), 69 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..e411586 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jwt-opa/build.gradle b/jwt-opa/build.gradle index 3372654..65e5a63 100644 --- a/jwt-opa/build.gradle +++ b/jwt-opa/build.gradle @@ -17,10 +17,10 @@ */ plugins { - id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'io.spring.dependency-management' version '1.1.3' id 'java' id 'jacoco' - id 'org.springframework.boot' version '2.5.7' + id 'org.springframework.boot' version '3.1.5' // To upload the Artifact to Maven Central // See: https://docs.gradle.org/current/userguide/publishing_maven.html @@ -38,7 +38,7 @@ ext { } group 'com.alertavert' -version '0.9.0' +version '0.10.0' // OpenJDK 17 LTS is the only Java version supported sourceCompatibility = JavaVersion.VERSION_17 @@ -78,7 +78,6 @@ dependencies { // See: https://stackoverflow.com/questions/29805622/could-not-find-or-load-main-class-org-gradle-wrapper-gradlewrappermain/31622432 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" annotationProcessor "org.projectlombok:lombok:${lombokVersion}" compileOnly "org.projectlombok:lombok:${lombokVersion}" @@ -86,16 +85,20 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'commons-codec:commons-codec:1.13' - testImplementation('org.springframework.boot:spring-boot-starter-test') { - exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' - } - + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webflux' // AWS SDK for Secrets Manager, see: https://docs.aws.amazon.com/code-samples/latest/catalog/code-catalog-javav2-example_code-secretsmanager.html implementation "software.amazon.awssdk:secretsmanager:${awsSdkVersion}" + // For the @PostConstruct annotation + implementation 'javax.annotation:javax.annotation-api:1.3.2' + + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + testImplementation "com.jayway.jsonpath:json-path-assert:$jsonpathVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" @@ -104,7 +107,6 @@ dependencies { testImplementation "org.testcontainers:junit-jupiter:$tcVersion" testImplementation "org.testcontainers:localstack:$tcVersion" testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.12.326' - } jacocoTestCoverageVerification { diff --git a/jwt-opa/src/main/java/com/alertavert/opa/jwt/ApiTokenAuthenticationFactory.java b/jwt-opa/src/main/java/com/alertavert/opa/jwt/ApiTokenAuthenticationFactory.java index be0c405..b40e77c 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/jwt/ApiTokenAuthenticationFactory.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/jwt/ApiTokenAuthenticationFactory.java @@ -44,8 +44,11 @@ @Service @Slf4j public class ApiTokenAuthenticationFactory { - @Autowired - JwtTokenProvider provider; + private final JwtTokenProvider provider; + + public ApiTokenAuthenticationFactory(JwtTokenProvider provider) { + this.provider = provider; + } /** * Creates an implementation of the {@link Authentication} interface which implements the @@ -62,7 +65,7 @@ public Mono createAuthentication(String token) { log.debug("Authenticating token {}...", token.substring(0, Math.min(MAX_TOKEN_LEN_LOG, token.length()))); try { DecodedJWT jwt = provider.decode(token); - List authorities = AuthorityUtils.createAuthorityList( + List authorities = AuthorityUtils.createAuthorityList( jwt.getClaim(JwtTokenProvider.ROLES).asArray(String.class)); String subject = jwt.getSubject(); @@ -72,6 +75,12 @@ public Mono createAuthentication(String token) { } catch (JWTVerificationException exception) { log.warn("Cannot validate API Token: {}", exception.getMessage()); return Mono.error(new BadCredentialsException("API Token invalid", exception)); + } catch (IllegalArgumentException exception) { + log.warn("The Token is malformed: {}", exception.getMessage()); + return Mono.error(new BadCredentialsException("API Token malformed", exception)); + } catch (Exception ex) { + log.error("Unexpected error while validating token: {}", ex.getMessage()); + return Mono.error(new BadCredentialsException("API Token malformed")); } } } diff --git a/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java b/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java index 1b57949..b09ad22 100644 --- a/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java +++ b/jwt-opa/src/main/java/com/alertavert/opa/security/OpaReactiveAuthorizationManager.java @@ -105,7 +105,7 @@ public Mono check( String path = request.getPath().toString(); for (String pattern : authRoutes) { if (pathMatcher.match(pattern, path)) { - log.debug("Route is allowed to bypass authorization"); + log.debug("Route {} is allowed to bypass authorization (matches: {})", path, pattern); return Mono.just(new AuthorizationDecision(true)); } } diff --git a/webapp-example/build.gradle b/webapp-example/build.gradle index c57b489..cc406d0 100644 --- a/webapp-example/build.gradle +++ b/webapp-example/build.gradle @@ -19,12 +19,12 @@ plugins { id 'java' id 'jacoco' - id 'org.springframework.boot' version '2.5.7' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'org.springframework.boot' version '3.1.5' + id 'io.spring.dependency-management' version '1.1.3' } group 'com.alertavert.opademo' -version = "0.3.0" +version = "0.4.0" repositories { // Adding local repository for Gradle to find jwt-opa before it gets published. @@ -36,7 +36,7 @@ repositories { ext { // This can be changed to an yet-unpublished version by using mavenLocal() // for local tests. - jwtOpaVersion = "0.9.0" + jwtOpaVersion = "0.10.0" lombokVersion = "1.18.22" tcVersion = "1.15.1" } @@ -48,9 +48,14 @@ bootJar { dependencies { // We use the actual dependency here, instead of depending on the module in the repository so // as to emulate an actual project using jwt-opa externally. + // Uncomment the following line (and comment out the one below) to use the local version + // while developing. implementation project (':jwt-opa') // implementation "com.alertavert:jwt-opa:${jwtOpaVersion}" + // For the @PostConstruct annotation + implementation 'javax.annotation:javax.annotation-api:1.3.2' + compileOnly "org.projectlombok:lombok:${lombokVersion}" annotationProcessor "org.projectlombok:lombok:${lombokVersion}" @@ -64,6 +69,9 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'commons-codec:commons-codec:1.13' + // For the @PostConstruct annotation + implementation 'javax.annotation:javax.annotation-api:1.3.2' + // Swagger 2 API & UI // "Raw" JSON at http://localhost:8081/v2/api-docs // UI at http://localhost:8081/swagger-ui/ (trailing slash matters) diff --git a/webapp-example/src/main/java/com/alertavert/opademo/DbInit.java b/webapp-example/src/main/java/com/alertavert/opademo/DbInit.java index 2b64cb0..a56367e 100644 --- a/webapp-example/src/main/java/com/alertavert/opademo/DbInit.java +++ b/webapp-example/src/main/java/com/alertavert/opademo/DbInit.java @@ -21,9 +21,7 @@ import com.alertavert.opademo.api.UserController; import com.alertavert.opademo.data.User; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -35,12 +33,10 @@ /** * Initializes the DB with a seed `admin` user and a random password, if it doesn't already exist. */ -@Profile("debug") @Slf4j @Component public class DbInit { - @Autowired - UserController controller; + private final UserController controller; @Value("${db.admin.username:admin}") String adminUsername; @@ -48,6 +44,10 @@ public class DbInit { @Value("${db.admin.password}") String adminPassword; + public DbInit(UserController controller) { + this.controller = controller; + } + @PostConstruct public void initDb() { @@ -57,7 +57,7 @@ public void initDb() { adminUsername, adminPassword); } User admin = new User(adminUsername, adminPassword, "SYSTEM"); - + log.info("Creating admin user: {}", adminUsername); controller.create(admin) .doOnSuccess(responseEntity -> { if (!responseEntity.getStatusCode().equals(HttpStatus.CREATED)) { @@ -68,7 +68,7 @@ public void initDb() { } }) .doOnError(ResponseStatusException.class, ex -> { - if (ex.getStatus().equals(HttpStatus.CONFLICT)) { + if (ex.getStatusCode().equals(HttpStatus.CONFLICT)) { log.info("User [{}] already exists in database, use existing credentials", adminUsername); } else { @@ -76,6 +76,7 @@ public void initDb() { System.exit(1); } }) + .onErrorComplete() .subscribe(); } } diff --git a/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java b/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java index f95f367..6200f7e 100644 --- a/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java +++ b/webapp-example/src/main/java/com/alertavert/opademo/api/JwtController.java @@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.jackson.Jacksonized; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -54,14 +56,7 @@ public JwtController(JwtTokenProvider provider, ReactiveUsersRepository reposito this.repository = repository; } - @Data - @AllArgsConstructor - static class ApiToken { - String username; - List roles; - @JsonProperty(API_TOKEN) - String apiToken; - } + record ApiToken(String username, List roles, @JsonProperty(API_TOKEN) String apiToken) { } @GetMapping(path = "/token/{user}", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) public Mono> getToken(@PathVariable String user) { diff --git a/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java b/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java index bc700e3..cb09e5b 100644 --- a/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java +++ b/webapp-example/src/main/java/com/alertavert/opademo/api/LoginController.java @@ -18,16 +18,12 @@ package com.alertavert.opademo.api; -import com.alertavert.opademo.DbInit; -import com.alertavert.opademo.data.ReactiveUsersRepository; import com.alertavert.opa.jwt.JwtTokenProvider; +import com.alertavert.opademo.data.ReactiveUsersRepository; import com.alertavert.opademo.data.User; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.util.Base64Utils; import org.springframework.util.MimeTypeUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -38,6 +34,7 @@ import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.UUID; import static com.alertavert.opa.Constants.BASIC_AUTH; @@ -56,14 +53,13 @@ consumes = MimeTypeUtils.ALL_VALUE) public class LoginController { - @Autowired - JwtTokenProvider provider; + private final JwtTokenProvider provider; + private final ReactiveUsersRepository repository; - @Autowired - ReactiveUsersRepository repository; - - @Autowired - PasswordEncoder encoder; + public LoginController(JwtTokenProvider provider, ReactiveUsersRepository repository) { + this.provider = provider; + this.repository = repository; + } @GetMapping @@ -82,7 +78,7 @@ Mono login( }) .doOnNext(apiToken -> log.debug("User authenticated, user = {}, token = {}...", - apiToken.getUsername(), apiToken.getApiToken().substring(0, MAX_TOKEN_LEN_LOG))); + apiToken.username(), apiToken.apiToken().substring(0, MAX_TOKEN_LEN_LOG))); } @GetMapping("/reset/{username}") @@ -114,7 +110,7 @@ public static Mono usernameFromHeader(String credentials) { log.debug("Extracting username from Authorization header"); if (credentials.startsWith(BASIC_AUTH)) { return Mono.just(credentials.substring(BASIC_AUTH.length() + 1)) - .map(enc -> Base64Utils.decode(enc.getBytes(StandardCharsets.UTF_8))) + .map(enc -> Base64.getDecoder().decode(enc.getBytes(StandardCharsets.UTF_8))) .map(String::new) .map(creds -> { String[] userPass = creds.split(":"); @@ -126,7 +122,7 @@ public static Mono usernameFromHeader(String credentials) { } public static Mono credentialsToHeader(String credentials) { - String encoded = Base64Utils.encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); return Mono.just(String.format("%s %s", BASIC_AUTH, encoded)); } } diff --git a/webapp-example/src/main/resources/application.yaml b/webapp-example/src/main/resources/application.yaml index ec0671a..3150964 100644 --- a/webapp-example/src/main/resources/application.yaml +++ b/webapp-example/src/main/resources/application.yaml @@ -101,7 +101,7 @@ keys: # For a PASSPHRASE, the secret is simply read from SecretsManager/Vault # The keypair is stored as a JSON-formatted secret, with two keys: "priv" and "pub". location: keypair - name: ../private/ec-key + name: private/ec-key logging: level: @@ -131,7 +131,7 @@ routes: - "/health" - "/demo" - "/favicon.ico" - - "/login/reset/*" + #- "/login/reset/*" # These will require the user to authenticate, but will not # be subject to OPA Policies authorization check. diff --git a/webapp-example/src/test/java/com/alertavert/opademo/api/LoginControllerTest.java b/webapp-example/src/test/java/com/alertavert/opademo/api/LoginControllerTest.java index 834e67f..874ade0 100644 --- a/webapp-example/src/test/java/com/alertavert/opademo/api/LoginControllerTest.java +++ b/webapp-example/src/test/java/com/alertavert/opademo/api/LoginControllerTest.java @@ -38,17 +38,6 @@ class LoginControllerTest { User bob, pete; - /** - * Takes a User with the password field in plaintext, and converts into a hashed one, then saves - * it to the DB. - * - * @param user - * @return the same user, but with a hashed password - */ - private Flux hashPasswordAndSave(User user) { - return hashPasswordAndSaveAll(List.of(user)); - } - private Flux hashPasswordAndSaveAll(List users) { return repository.saveAll( users.stream() @@ -72,18 +61,18 @@ public void validUserSuccessfullyLogin() { .exchange() .expectStatus().isOk() .expectBody(JwtController.ApiToken.class) - .value(t -> assertThat(t.getUsername().equals("bob"))) + .value(t -> assertThat(t.username().equals("bob"))) .returnResult() .getResponseBody(); assertThat(apiToken).isNotNull(); - assertThat(apiToken.getUsername()).isEqualTo("bob"); - assertThat(apiToken.getRoles()).contains("USER"); - assertThat(apiToken.getApiToken()).isNotEmpty(); + assertThat(apiToken.username()).isEqualTo("bob"); + assertThat(apiToken.roles()).contains("USER"); + assertThat(apiToken.apiToken()).isNotEmpty(); } @Test - public void validUserWrongPwdFailsLogin() { + void validUserWrongPwdFailsLogin() { client.get() .uri("/login") .header(HttpHeaders.AUTHORIZATION, LoginController.credentialsToHeader("bob:foo").block()) @@ -92,7 +81,7 @@ public void validUserWrongPwdFailsLogin() { } @Test - public void invalidUserFailsLogin() { + void invalidUserFailsLogin() { client.get() .uri("/login") .header(HttpHeaders.AUTHORIZATION, @@ -102,7 +91,7 @@ public void invalidUserFailsLogin() { } @Test - public void validUserCanResetPassword() { + void validUserCanResetPassword() { client.get() .uri("/login/reset/pete") .exchange() diff --git a/webapp-example/src/test/java/com/alertavert/opademo/data/ReactiveUsersRepositoryTest.java b/webapp-example/src/test/java/com/alertavert/opademo/data/ReactiveUsersRepositoryTest.java index 1f33a82..fd53aa5 100644 --- a/webapp-example/src/test/java/com/alertavert/opademo/data/ReactiveUsersRepositoryTest.java +++ b/webapp-example/src/test/java/com/alertavert/opademo/data/ReactiveUsersRepositoryTest.java @@ -106,7 +106,7 @@ void findByRole() { } @Test - void jsongen() throws JsonProcessingException { + void jsongen() throws Exception { ObjectMapper mapper = new ObjectMapper(); User me = new User("me", "myself", "USER");