From d0e6fdcbc77b8d3fb532167ab8030e070d2868ba Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Wed, 26 Jun 2024 09:44:50 +0200 Subject: [PATCH 1/6] CID-2732: authenticate to GHE --- build.gradle.kts | 16 ++++ .../githubagent/GithubAgentApplication.kt | 4 + .../leanix/githubagent/client/GithubClient.kt | 15 ++++ .../config/AgentSetupValidation.kt | 30 +++++++ .../config/GithubEnterpriseProperties.kt | 10 +++ .../githubagent/dto/GithubAppResponse.kt | 9 ++ .../githubagent/exceptions/Exceptions.kt | 7 ++ .../githubagent/runners/PostStartupRunner.kt | 14 ++++ .../githubagent/services/CachingService.kt | 26 ++++++ .../services/GithubAuthenticationService.kt | 83 +++++++++++++++++++ src/main/resources/application.yaml | 4 + src/test/resources/application.yaml | 4 + 12 files changed, 222 insertions(+) create mode 100644 src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/services/CachingService.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt create mode 100644 src/main/resources/application.yaml create mode 100644 src/test/resources/application.yaml diff --git a/build.gradle.kts b/build.gradle.kts index 3d9e1d9..015845e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,9 +20,25 @@ repositories { mavenCentral() } +extra["springCloudVersion"] = "2023.0.1" + +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + dependencies { implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("io.jsonwebtoken:jjwt-api:0.11.2") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.bouncycastle:bcprov-jdk18on:1.78") + implementation("org.bouncycastle:bcpkix-jdk18on:1.78") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt b/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt index 58a5e51..54ca4dc 100644 --- a/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt +++ b/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt @@ -1,9 +1,13 @@ package net.leanix.githubagent import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication +import org.springframework.cloud.openfeign.EnableFeignClients @SpringBootApplication +@EnableFeignClients(value = ["net.leanix.githubagent.client"]) +@EnableConfigurationProperties(value = [net.leanix.githubagent.config.GithubEnterpriseProperties::class]) class GithubAgentApplication fun main() { diff --git a/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt b/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt new file mode 100644 index 0000000..e32aa3a --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt @@ -0,0 +1,15 @@ +package net.leanix.githubagent.client + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestHeader + +@FeignClient(name = "githubClient", url = "\${github-enterprise.baseUrl}") +interface GithubClient { + + @GetMapping("/api/v3/app") + fun getApp( + @RequestHeader("Authorization") jwt: String, + @RequestHeader("Accept") accept: String = "application/vnd.github.v3+json" + ): String +} diff --git a/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt b/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt new file mode 100644 index 0000000..f10e5c9 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt @@ -0,0 +1,30 @@ +package net.leanix.githubagent.config + +import jakarta.annotation.PostConstruct +import net.leanix.githubagent.exceptions.GithubEnterpriseConfigurationMissingException +import org.springframework.stereotype.Component + +@Component +class AgentSetupValidation( + private val githubEnterpriseProperties: GithubEnterpriseProperties +) { + + @PostConstruct + fun validateConfiguration() { + val missingProperties = mutableListOf() + + if (githubEnterpriseProperties.baseUrl.isBlank()) { + missingProperties.add("GITHUB_ENTERPRISE_BASE_URL") + } + if (githubEnterpriseProperties.githubAppId.isBlank()) { + missingProperties.add("GITHUB_ENTERPRISE_GITHUB_APP_ID") + } + if (githubEnterpriseProperties.pemFile.isBlank()) { + missingProperties.add("GITHUB_ENTERPRISE_PEM_FILE") + } + + if (missingProperties.isNotEmpty()) { + throw GithubEnterpriseConfigurationMissingException(missingProperties.joinToString(", ")) + } + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt b/src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt new file mode 100644 index 0000000..6dae6e3 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt @@ -0,0 +1,10 @@ +package net.leanix.githubagent.config + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "github-enterprise") +data class GithubEnterpriseProperties( + val baseUrl: String, + val githubAppId: String, + val pemFile: String, +) diff --git a/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt b/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt new file mode 100644 index 0000000..b75588f --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt @@ -0,0 +1,9 @@ +package net.leanix.githubagent.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +data class GithubAppResponse( + @JsonProperty("name") val name: String +) diff --git a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt new file mode 100644 index 0000000..0313795 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt @@ -0,0 +1,7 @@ +package net.leanix.githubagent.exceptions + +class GithubEnterpriseConfigurationMissingException(properties: String) : RuntimeException( + "Github Enterprise properties '$properties' are not set" +) +class AuthenticationFailedException(message: String) : RuntimeException(message) +class ConnectingToGithubEnterpriseFailedException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt new file mode 100644 index 0000000..d6c3a89 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt @@ -0,0 +1,14 @@ +package net.leanix.githubagent.runners + +import net.leanix.githubagent.services.GithubAuthenticationService +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.stereotype.Component + +@Component +class PostStartupRunner(private val githubAuthenticationService: GithubAuthenticationService) : ApplicationRunner { + + override fun run(args: ApplicationArguments?) { + githubAuthenticationService.generateJwtToken() + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt new file mode 100644 index 0000000..24f9c3b --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt @@ -0,0 +1,26 @@ +package net.leanix.githubagent.services + +import jakarta.annotation.PostConstruct +import net.leanix.githubagent.config.GithubEnterpriseProperties +import org.springframework.stereotype.Service + +@Service +class CachingService( + private val githubEnterpriseProperties: GithubEnterpriseProperties +) { + private val cache = HashMap() + + @PostConstruct + private fun init() { + cache["baseUrl"] = githubEnterpriseProperties.baseUrl + cache["githubAppId"] = githubEnterpriseProperties.githubAppId + } + + fun set(key: String, value: String) { + cache[key] = value + } + + fun get(key: String): String? { + return cache[key] + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt new file mode 100644 index 0000000..3db7ee8 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt @@ -0,0 +1,83 @@ +package net.leanix.githubagent.services + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import net.leanix.githubagent.client.GithubClient +import net.leanix.githubagent.config.GithubEnterpriseProperties +import net.leanix.githubagent.dto.GithubAppResponse +import net.leanix.githubagent.exceptions.AuthenticationFailedException +import net.leanix.githubagent.exceptions.ConnectingToGithubEnterpriseFailedException +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.slf4j.LoggerFactory +import org.springframework.core.io.ResourceLoader +import org.springframework.stereotype.Service +import java.io.File +import java.io.IOException +import java.nio.charset.Charset +import java.nio.file.Files +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.Security +import java.security.spec.PKCS8EncodedKeySpec +import java.util.* + +@Service +class GithubAuthenticationService( + private val cachingService: CachingService, + private val githubClient: GithubClient, + private val githubEnterpriseProperties: GithubEnterpriseProperties, + private val resourceLoader: ResourceLoader +) { + + companion object { + private const val JWT_EXPIRATION_DURATION = 600000L + private val logger = LoggerFactory.getLogger(GithubAuthenticationService::class.java) + } + + fun generateJwtToken() { + runCatching { + logger.info("Generating JWT token") + Security.addProvider(BouncyCastleProvider()) + val rsaPrivateKey: String = readPrivateKey(loadPemFile()) + val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey)) + val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec) + createJwtToken(privateKey)?.also { + cachingService.set("jwtToken", it) + verifyJwt(it) + } ?: throw AuthenticationFailedException("Failed to generate a valid JWT token") + } + } + + private fun createJwtToken(privateKey: PrivateKey): String? { + return Jwts.builder() + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION)) + .setIssuer(cachingService.get("githubAppId")) + .signWith(privateKey, SignatureAlgorithm.RS256) + .compact() + } + + @Throws(IOException::class) + private fun readPrivateKey(file: File): String { + return String(Files.readAllBytes(file.toPath()), Charset.defaultCharset()) + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace(System.lineSeparator().toRegex(), "") + .replace("-----END RSA PRIVATE KEY-----", "") + } + + private fun verifyJwt(jwt: String) { + runCatching { + val githubApp = jacksonObjectMapper().readValue( + githubClient.getApp("Bearer $jwt"), + GithubAppResponse::class.java + ) + logger.info("Authenticated as GitHub App: ${githubApp.name}") + }.onFailure { + throw ConnectingToGithubEnterpriseFailedException("Failed to verify JWT token") + } + } + + private fun loadPemFile() = + File(resourceLoader.getResource("file:${githubEnterpriseProperties.pemFile}").uri) +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..a6b5655 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +github-enterprise: + baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:} + githubAppId: ${GITHUB_APP_ID:} + pemFile: ${PEM_FILE:} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml new file mode 100644 index 0000000..aa42a24 --- /dev/null +++ b/src/test/resources/application.yaml @@ -0,0 +1,4 @@ +github-enterprise: + baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:dummy} + githubAppId: ${GITHUB_APP_ID:dummy} + pemFile: ${PEM_FILE:dummy} From 162ba7a4d753f4a9f959756fe4cf122fbd1f7a2a Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Thu, 27 Jun 2024 10:50:48 +0200 Subject: [PATCH 2/6] CID-2732: improve implementation --- build.gradle.kts | 6 +++--- .../net/leanix/githubagent/client/GithubClient.kt | 3 ++- .../leanix/githubagent/runners/PostStartupRunner.kt | 2 ++ .../services/GithubAuthenticationService.kt | 12 +++++------- .../githubagent/GithubAgentApplicationTests.kt | 2 ++ 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 015845e..e4d043d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,12 +36,12 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.bouncycastle:bcprov-jdk18on:1.78") implementation("org.bouncycastle:bcpkix-jdk18on:1.78") - runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") - runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") + implementation("io.jsonwebtoken:jjwt-impl:0.11.2") + implementation("io.jsonwebtoken:jjwt-jackson:0.11.2") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.junit.platform:junit-platform-launcher") } detekt { diff --git a/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt b/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt index e32aa3a..437afcd 100644 --- a/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt +++ b/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt @@ -1,5 +1,6 @@ package net.leanix.githubagent.client +import net.leanix.githubagent.dto.GithubAppResponse import org.springframework.cloud.openfeign.FeignClient import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestHeader @@ -11,5 +12,5 @@ interface GithubClient { fun getApp( @RequestHeader("Authorization") jwt: String, @RequestHeader("Accept") accept: String = "application/vnd.github.v3+json" - ): String + ): GithubAppResponse } diff --git a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt index d6c3a89..08bdce2 100644 --- a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt +++ b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt @@ -3,9 +3,11 @@ package net.leanix.githubagent.runners import net.leanix.githubagent.services.GithubAuthenticationService import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Profile import org.springframework.stereotype.Component @Component +@Profile("!test") class PostStartupRunner(private val githubAuthenticationService: GithubAuthenticationService) : ApplicationRunner { override fun run(args: ApplicationArguments?) { diff --git a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt index 3db7ee8..7670eec 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt @@ -1,11 +1,9 @@ package net.leanix.githubagent.services -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import net.leanix.githubagent.client.GithubClient import net.leanix.githubagent.config.GithubEnterpriseProperties -import net.leanix.githubagent.dto.GithubAppResponse import net.leanix.githubagent.exceptions.AuthenticationFailedException import net.leanix.githubagent.exceptions.ConnectingToGithubEnterpriseFailedException import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -45,7 +43,10 @@ class GithubAuthenticationService( createJwtToken(privateKey)?.also { cachingService.set("jwtToken", it) verifyJwt(it) - } ?: throw AuthenticationFailedException("Failed to generate a valid JWT token") + } + }.onFailure { + logger.error("Failed to generate/validate JWT token", it) + throw AuthenticationFailedException("Failed to generate a valid JWT token") } } @@ -68,10 +69,7 @@ class GithubAuthenticationService( private fun verifyJwt(jwt: String) { runCatching { - val githubApp = jacksonObjectMapper().readValue( - githubClient.getApp("Bearer $jwt"), - GithubAppResponse::class.java - ) + val githubApp = githubClient.getApp("Bearer $jwt") logger.info("Authenticated as GitHub App: ${githubApp.name}") }.onFailure { throw ConnectingToGithubEnterpriseFailedException("Failed to verify JWT token") diff --git a/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt b/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt index 0a55605..07458a3 100644 --- a/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt +++ b/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt @@ -2,8 +2,10 @@ package net.leanix.githubagent import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles @SpringBootTest +@ActiveProfiles("test") class GithubAgentApplicationTests { @Test From e116e60b93fb01898faea74b95bd7458b8a77b31 Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Thu, 27 Jun 2024 15:54:08 +0200 Subject: [PATCH 3/6] CID-2732: improve implementation, refactor, optimise dependencies and fix Snyk vulnerabilities --- build.gradle.kts | 22 ++++-- .../githubagent/dto/GithubAppResponse.kt | 4 +- .../githubagent/exceptions/Exceptions.kt | 5 +- .../services/GitHubEnterpriseService.kt | 50 +++++++++++++ .../services/GithubAuthenticationService.kt | 69 ++++++++--------- .../services/GitHubEnterpriseServiceTest.kt | 74 +++++++++++++++++++ .../GithubAuthenticationServiceTest.kt | 47 ++++++++++++ src/test/resources/invalid-private-key.pem | 27 +++++++ src/test/resources/valid-private-key.pem | 27 +++++++ 9 files changed, 282 insertions(+), 43 deletions(-) create mode 100644 src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt create mode 100644 src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt create mode 100644 src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt create mode 100644 src/test/resources/invalid-private-key.pem create mode 100644 src/test/resources/valid-private-key.pem diff --git a/build.gradle.kts b/build.gradle.kts index e4d043d..7cc88ac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,16 +32,24 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("io.jsonwebtoken:jjwt-api:0.11.2") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.bouncycastle:bcprov-jdk18on:1.78") - implementation("org.bouncycastle:bcpkix-jdk18on:1.78") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.17.1") + + // Dependencies for generating JWT token implementation("io.jsonwebtoken:jjwt-impl:0.11.2") implementation("io.jsonwebtoken:jjwt-jackson:0.11.2") testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testImplementation("org.junit.platform:junit-platform-launcher") + testImplementation("io.mockk:mockk:1.12.0") +} + +configurations.all { + resolutionStrategy { + eachDependency { + when (requested.module.toString()) { + "org.bouncycastle:bcprov-jdk18on" -> useVersion("1.78") + } + } + } } detekt { @@ -74,4 +82,4 @@ tasks.jacocoTestReport { xml.required.set(true) xml.outputLocation.set(File("${projectDir}/build/jacocoXml/jacocoTestReport.xml")) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt b/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt index b75588f..9880aee 100644 --- a/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt +++ b/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt @@ -5,5 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty @JsonIgnoreProperties(ignoreUnknown = true) data class GithubAppResponse( - @JsonProperty("name") val name: String + @JsonProperty("name") val name: String, + @JsonProperty("permissions") val permissions: Map, + @JsonProperty("events") val events: List ) diff --git a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt index 0313795..20bd154 100644 --- a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt +++ b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt @@ -3,5 +3,6 @@ package net.leanix.githubagent.exceptions class GithubEnterpriseConfigurationMissingException(properties: String) : RuntimeException( "Github Enterprise properties '$properties' are not set" ) -class AuthenticationFailedException(message: String) : RuntimeException(message) -class ConnectingToGithubEnterpriseFailedException(message: String) : RuntimeException(message) +class GithubAppInsufficientPermissionsException(message: String) : RuntimeException(message) +class FailedToCreateJWTException(message: String) : RuntimeException(message) +class UnableToConnectToGithubEnterpriseException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt new file mode 100644 index 0000000..418acdf --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt @@ -0,0 +1,50 @@ +package net.leanix.githubagent.services + +import net.leanix.githubagent.client.GithubClient +import net.leanix.githubagent.dto.GithubAppResponse +import net.leanix.githubagent.exceptions.GithubAppInsufficientPermissionsException +import net.leanix.githubagent.exceptions.UnableToConnectToGithubEnterpriseException +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class GitHubEnterpriseService(private val githubClient: GithubClient) { + + companion object { + val expectedPermissions = listOf("administration", "contents", "metadata") + val expectedEvents = listOf("label", "public", "repository") + } + private val logger = LoggerFactory.getLogger(GitHubEnterpriseService::class.java) + + fun verifyJwt(jwt: String) { + runCatching { + val githubApp = githubClient.getApp("Bearer $jwt") + validateGithubAppResponse(githubApp) + logger.info("Authenticated as GitHub App: '${githubApp.name}'") + }.onFailure { + when (it) { + is GithubAppInsufficientPermissionsException -> throw it + else -> throw UnableToConnectToGithubEnterpriseException("Failed to verify JWT token") + } + } + } + + fun validateGithubAppResponse(response: GithubAppResponse) { + val missingPermissions = expectedPermissions.filterNot { response.permissions.containsKey(it) } + val missingEvents = expectedEvents.filterNot { response.events.contains(it) } + + if (missingPermissions.isNotEmpty() || missingEvents.isNotEmpty()) { + var message = "GitHub App is missing the following " + if (missingPermissions.isNotEmpty()) { + message = message.plus("permissions: $missingPermissions") + } + if (missingEvents.isNotEmpty()) { + if (missingPermissions.isNotEmpty()) { + message = message.plus(", and the following") + } + message = message.plus("events: $missingEvents") + } + throw GithubAppInsufficientPermissionsException(message) + } + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt index 7670eec..72a7f9d 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt @@ -2,10 +2,8 @@ package net.leanix.githubagent.services import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm -import net.leanix.githubagent.client.GithubClient import net.leanix.githubagent.config.GithubEnterpriseProperties -import net.leanix.githubagent.exceptions.AuthenticationFailedException -import net.leanix.githubagent.exceptions.ConnectingToGithubEnterpriseFailedException +import net.leanix.githubagent.exceptions.FailedToCreateJWTException import org.bouncycastle.jce.provider.BouncyCastleProvider import org.slf4j.LoggerFactory import org.springframework.core.io.ResourceLoader @@ -17,19 +15,22 @@ import java.nio.file.Files import java.security.KeyFactory import java.security.PrivateKey import java.security.Security +import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.util.* @Service class GithubAuthenticationService( private val cachingService: CachingService, - private val githubClient: GithubClient, private val githubEnterpriseProperties: GithubEnterpriseProperties, - private val resourceLoader: ResourceLoader + private val resourceLoader: ResourceLoader, + private val gitHubEnterpriseService: GitHubEnterpriseService ) { companion object { private const val JWT_EXPIRATION_DURATION = 600000L + private const val pemPrefix = "-----BEGIN RSA PRIVATE KEY-----" + private const val pemSuffix = "-----END RSA PRIVATE KEY-----" private val logger = LoggerFactory.getLogger(GithubAuthenticationService::class.java) } @@ -37,45 +38,47 @@ class GithubAuthenticationService( runCatching { logger.info("Generating JWT token") Security.addProvider(BouncyCastleProvider()) - val rsaPrivateKey: String = readPrivateKey(loadPemFile()) + val rsaPrivateKey: String = readPrivateKey() val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey)) val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec) - createJwtToken(privateKey)?.also { - cachingService.set("jwtToken", it) - verifyJwt(it) - } + val jwt = createJwtToken(privateKey) + cachingService.set("jwtToken", jwt.getOrThrow()) + gitHubEnterpriseService.verifyJwt(jwt.getOrThrow()) }.onFailure { logger.error("Failed to generate/validate JWT token", it) - throw AuthenticationFailedException("Failed to generate a valid JWT token") + if (it is InvalidKeySpecException) { + throw IllegalArgumentException("The provided private key is not in a valid PKCS8 format.", it) + } else { + throw it + } } } - private fun createJwtToken(privateKey: PrivateKey): String? { - return Jwts.builder() - .setIssuedAt(Date()) - .setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION)) - .setIssuer(cachingService.get("githubAppId")) - .signWith(privateKey, SignatureAlgorithm.RS256) - .compact() + private fun createJwtToken(privateKey: PrivateKey): Result { + return runCatching { + Jwts.builder() + .setIssuedAt(Date()) + .setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION)) + .setIssuer(cachingService.get("githubAppId")) + .signWith(privateKey, SignatureAlgorithm.RS256) + .compact() + }.onFailure { + throw FailedToCreateJWTException("Failed to generate a valid JWT token") + } } @Throws(IOException::class) - private fun readPrivateKey(file: File): String { - return String(Files.readAllBytes(file.toPath()), Charset.defaultCharset()) - .replace("-----BEGIN RSA PRIVATE KEY-----", "") - .replace(System.lineSeparator().toRegex(), "") - .replace("-----END RSA PRIVATE KEY-----", "") - } + private fun readPrivateKey(): String { + val pemFile = File(resourceLoader.getResource("file:${githubEnterpriseProperties.pemFile}").uri) + val fileContent = String(Files.readAllBytes(pemFile.toPath()), Charset.defaultCharset()).trim() - private fun verifyJwt(jwt: String) { - runCatching { - val githubApp = githubClient.getApp("Bearer $jwt") - logger.info("Authenticated as GitHub App: ${githubApp.name}") - }.onFailure { - throw ConnectingToGithubEnterpriseFailedException("Failed to verify JWT token") + require(fileContent.startsWith(pemPrefix) && fileContent.endsWith(pemSuffix)) { + "The provided file is not a valid PEM file." } - } - private fun loadPemFile() = - File(resourceLoader.getResource("file:${githubEnterpriseProperties.pemFile}").uri) + return fileContent + .replace(pemPrefix, "") + .replace(System.lineSeparator().toRegex(), "") + .replace(pemSuffix, "") + } } diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt new file mode 100644 index 0000000..92c88a7 --- /dev/null +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt @@ -0,0 +1,74 @@ +import io.mockk.every +import io.mockk.mockk +import net.leanix.githubagent.client.GithubClient +import net.leanix.githubagent.dto.GithubAppResponse +import net.leanix.githubagent.exceptions.GithubAppInsufficientPermissionsException +import net.leanix.githubagent.exceptions.UnableToConnectToGithubEnterpriseException +import net.leanix.githubagent.services.GitHubEnterpriseService +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +class GitHubEnterpriseServiceTest { + + private val githubClient = mockk() + private val service = GitHubEnterpriseService(githubClient) + + @Test + fun `verifyJwt with valid jwt should not throw exception`() { + val jwt = "validJwt" + val githubApp = GithubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), + events = listOf("label", "public", "repository") + ) + every { githubClient.getApp(any()) } returns githubApp + + assertDoesNotThrow { service.verifyJwt(jwt) } + } + + @Test + fun `verifyJwt with invalid jwt should throw exception`() { + val jwt = "invalidJwt" + every { githubClient.getApp(any()) } throws Exception() + + assertThrows(UnableToConnectToGithubEnterpriseException::class.java) { service.verifyJwt(jwt) } + } + + @Test + fun `validateGithubAppResponse with correct permissions should not throw exception`() { + val response = GithubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), + events = listOf("label", "public", "repository") + ) + + assertDoesNotThrow { service.validateGithubAppResponse(response) } + } + + @Test + fun `validateGithubAppResponse with missing permissions should throw exception`() { + val response = GithubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read"), + events = listOf("label", "public", "repository") + ) + + assertThrows( + GithubAppInsufficientPermissionsException::class.java + ) { service.validateGithubAppResponse(response) } + } + + @Test + fun `validateGithubAppResponse with missing events should throw exception`() { + val response = GithubAppResponse( + name = "validApp", + permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), + events = listOf("label", "public") + ) + + assertThrows( + GithubAppInsufficientPermissionsException::class.java + ) { service.validateGithubAppResponse(response) } + } +} diff --git a/src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt new file mode 100644 index 0000000..80659b3 --- /dev/null +++ b/src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt @@ -0,0 +1,47 @@ +import io.mockk.every +import io.mockk.mockk +import net.leanix.githubagent.config.GithubEnterpriseProperties +import net.leanix.githubagent.services.CachingService +import net.leanix.githubagent.services.GitHubEnterpriseService +import net.leanix.githubagent.services.GithubAuthenticationService +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.ResourceLoader + +class GithubAuthenticationServiceTest { + + private val cachingService = mockk() + private val githubEnterpriseProperties = mockk() + private val resourceLoader = mockk() + private val gitHubEnterpriseService = mockk() + private val githubAuthenticationService = GithubAuthenticationService( + cachingService, + githubEnterpriseProperties, + resourceLoader, + gitHubEnterpriseService + ) + + @Test + fun `generateJwtToken with valid data should not throw exception`() { + every { cachingService.get(any()) } returns "dummy-value" + every { cachingService.set(any(), any()) } returns Unit + every { githubEnterpriseProperties.pemFile } returns "valid-private-key.pem" + every { resourceLoader.getResource(any()) } returns ClassPathResource("valid-private-key.pem") + every { gitHubEnterpriseService.verifyJwt(any()) } returns Unit + + assertDoesNotThrow { githubAuthenticationService.generateJwtToken() } + assertNotNull(cachingService.get("jwtToken")) + } + + @Test + fun `generateJwtToken with invalid data should throw exception`() { + every { cachingService.get(any()) } returns "dummy-value" + every { githubEnterpriseProperties.pemFile } returns "invalid-private-key.pem" + every { resourceLoader.getResource(any()) } returns ClassPathResource("invalid-private-key.pem") + + assertThrows(IllegalArgumentException::class.java) { githubAuthenticationService.generateJwtToken() } + } +} diff --git a/src/test/resources/invalid-private-key.pem b/src/test/resources/invalid-private-key.pem new file mode 100644 index 0000000..c5ccba8 --- /dev/null +++ b/src/test/resources/invalid-private-key.pem @@ -0,0 +1,27 @@ + +MIIEpAIBAAKCAQEAwqjwjl1IJ2Mo4TMtNmAoZl+lnRP88D2ocMrj1QgKYzHTnsAW +UudOX909Mxbjj9ZKpuDpggL3/X+h/pCaD7yhS1OgYo1pl8TmWwmDq8ok0VJYlfxi +3oH76kexyQZ+SYT7YqZ7Xy67Q/kcjDyVK708vnKdhEaGFCVdIxbUfzsIynq6xbKx +PETEMlW1dBHQSrwIYgGTAwKTrvqmpNxZw7yIFA6qASddQmpbm0ycoMXYVrz+Nac6 +RrR93YVY3Jc+0c13bSenCqlRtMEHLfmGTuKboKiQWRgS16CEgfg2b29310OtLC6T +PSk9Dtv1knrVjpnWVaMq3w28ky3I1aeoKZCkPQIDAQABAoIBAQCWEJ0ac0k7rBMI +wWY6hBjBCz1mgdE995qSEadgRImVfQUSXi0Xjl/6QVl7uEqISYBVdBAv/U/m6m0d +DabnONjzdC2xrCjaKp4XUpdiaTzG7f+C6QXjWTu2mbyyJ8JVtSIDJCr57tHJDhN2 +/QFWrdVVUJCkN6YHg+JwOZpp1z3osSldnRCYUJ7NcPfNYCj/n0Gq5fQ3MUmk17ch +O5+XOxa8GBFj9hCqqFB97qnYSkRDTv0YoLdlIdnnVQeKYYMFCdKa++vgHX/7Pu8B +KFr34Fm1BFjkoIYjOtYbeUf2lWG+dzwEwLUu5DUcYS+YyUBCogUDLtROHScPSSFU +5hHin6S1AoGBAPHde46hvPmBNR6DGkds1twavbvEynlKiKdpgWn+ycBaLOPXO/hb +xdjAohZNIYwE72ggYWnMhHy1OnhytUMopMsT/xbDu+v5iwF+/9x9C7gdBj8drEzx +4E86O+lQ7ROh1PoAPwTqFUY0rEmsJRvfTY8oUp9LuiPWuO5Mc1tGIjJXAoGBAM4J +OYVKqc5Rzt4pSWzy3wzxekE1XVN7SRdcdYyjqOiYRLmc1jSx5nuTotluSd/trtZw +5Sf65e9YkO2zx5Ou4/TWdnGurWP8BgBAT2bDCDKjetiJTHSB68Hcz0zfH99C9h+E +8vn8Lpn57fFG+TOiADBPAYNEEkBxBJyGn4d+r8mLAoGBAISRIhT2f46+DDByKWg2 +trmjipUtctDyUl54TK+dMFXW1z32je891f5M70qL8jQ9zD7laJ9FsuRrrOWx8boi +v9hzWGDQ3eKkP1WNl43xmAfNGMxlZjgyZwDl6UqjyZ32GLcChYgbCZgWbMxgp2JU +jb1Gm6qmJhtYqLosexnvIfU3AoGAR5znNFAmQ0MmDwv0rHyiUIJiRuYAgTK5zffi +F7cOz4GVaZp8zaYEAXHoSYDPBpk7iueEjufjIdT70tMJDGjebMxaMNtRAw6nG1E/ +B+3EHK271iWqwFgkFKbmGsb28gf5Oi1gsskXfYdkT9emaG7nd+MOGI0BdwqRWsJk +EplTCk8CgYATcdreHFdXBCbRLszoiPPpvNTi0lBUdor+PzVrewAdByOY9dajBbap +2Fbuu2fkhBPEP8BL+3fJmbXsVVxOf9Nzy/IusekfuC5ZGnc41aCtaC6hplaXs131 +UvAdbhohImJi8D/p6uXPvrwrApBvoDpEu3Sq36VMCPeSv3YmTngLXw== +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/valid-private-key.pem b/src/test/resources/valid-private-key.pem new file mode 100644 index 0000000..dd61cc2 --- /dev/null +++ b/src/test/resources/valid-private-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwqjwjl1IJ2Mo4TMtNmAoZl+lnRP88D2ocMrj1QgKYzHTnsAW +UudOX909Mxbjj9ZKpuDpggL3/X+h/pCaD7yhS1OgYo1pl8TmWwmDq8ok0VJYlfxi +3oH76kexyQZ+SYT7YqZ7Xy67Q/kcjDyVK708vnKdhEaGFCVdIxbUfzsIynq6xbKx +PETEMlW1dBHQSrwIYgGTAwKTrvqmpNxZw7yIFA6qASddQmpbm0ycoMXYVrz+Nac6 +RrR93YVY3Jc+0c13bSenCqlRtMEHLfmGTuKboKiQWRgS16CEgfg2b29310OtLC6T +PSk9Dtv1knrVjpnWVaMq3w28ky3I1aeoKZCkPQIDAQABAoIBAQCWEJ0ac0k7rBMI +wWY6hBjBCz1mgdE995qSEadgRImVfQUSXi0Xjl/6QVl7uEqISYBVdBAv/U/m6m0d +DabnONjzdC2xrCjaKp4XUpdiaTzG7f+C6QXjWTu2mbyyJ8JVtSIDJCr57tHJDhN2 +/QFWrdVVUJCkN6YHg+JwOZpp1z3osSldnRCYUJ7NcPfNYCj/n0Gq5fQ3MUmk17ch +O5+XOxa8GBFj9hCqqFB97qnYSkRDTv0YoLdlIdnnVQeKYYMFCdKa++vgHX/7Pu8B +KFr34Fm1BFjkoIYjOtYbeUf2lWG+dzwEwLUu5DUcYS+YyUBCogUDLtROHScPSSFU +5hHin6S1AoGBAPHde46hvPmBNR6DGkds1twavbvEynlKiKdpgWn+ycBaLOPXO/hb +xdjAohZNIYwE72ggYWnMhHy1OnhytUMopMsT/xbDu+v5iwF+/9x9C7gdBj8drEzx +4E86O+lQ7ROh1PoAPwTqFUY0rEmsJRvfTY8oUp9LuiPWuO5Mc1tGIjJXAoGBAM4J +OYVKqc5Rzt4pSWzy3wzxekE1XVN7SRdcdYyjqOiYRLmc1jSx5nuTotluSd/trtZw +5Sf65e9YkO2zx5Ou4/TWdnGurWP8BgBAT2bDCDKjetiJTHSB68Hcz0zfH99C9h+E +8vn8Lpn57fFG+TOiADBPAYNEEkBxBJyGn4d+r8mLAoGBAISRIhT2f46+DDByKWg2 +trmjipUtctDyUl54TK+dMFXW1z32je891f5M70qL8jQ9zD7laJ9FsuRrrOWx8boi +v9hzWGDQ3eKkP1WNl43xmAfNGMxlZjgyZwDl6UqjyZ32GLcChYgbCZgWbMxgp2JU +jb1Gm6qmJhtYqLosexnvIfU3AoGAR5znNFAmQ0MmDwv0rHyiUIJiRuYAgTK5zffi +F7cOz4GVaZp8zaYEAXHoSYDPBpk7iueEjufjIdT70tMJDGjebMxaMNtRAw6nG1E/ +B+3EHK271iWqwFgkFKbmGsb28gf5Oi1gsskXfYdkT9emaG7nd+MOGI0BdwqRWsJk +EplTCk8CgYATcdreHFdXBCbRLszoiPPpvNTi0lBUdor+PzVrewAdByOY9dajBbap +2Fbuu2fkhBPEP8BL+3fJmbXsVVxOf9Nzy/IusekfuC5ZGnc41aCtaC6hplaXs131 +UvAdbhohImJi8D/p6uXPvrwrApBvoDpEu3Sq36VMCPeSv3YmTngLXw== +-----END RSA PRIVATE KEY----- From 30ef674b60e18ea1af460786f15ff3eea30e9e3f Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Thu, 4 Jul 2024 13:24:51 +0200 Subject: [PATCH 4/6] CID-2732: Address PR comments --- ...plication.kt => GitHubAgentApplication.kt} | 6 ++--- .../{GithubClient.kt => GitHubClient.kt} | 6 ++--- .../config/AgentSetupValidation.kt | 6 ++--- ...rties.kt => GitHubEnterpriseProperties.kt} | 2 +- ...hubAppResponse.kt => GitHubAppResponse.kt} | 2 +- .../githubagent/exceptions/Exceptions.kt | 6 ++--- .../githubagent/runners/PostStartupRunner.kt | 4 ++-- .../githubagent/services/CachingService.kt | 4 ++-- ...vice.kt => GitHubAuthenticationService.kt} | 8 +++---- .../services/GitHubEnterpriseService.kt | 19 ++++++++------- ...ests.kt => GitHubAgentApplicationTests.kt} | 2 +- ....kt => GitHubAuthenticationServiceTest.kt} | 10 ++++---- .../services/GitHubEnterpriseServiceTest.kt | 24 +++++++++---------- 13 files changed, 50 insertions(+), 49 deletions(-) rename src/main/kotlin/net/leanix/githubagent/{GithubAgentApplication.kt => GitHubAgentApplication.kt} (79%) rename src/main/kotlin/net/leanix/githubagent/client/{GithubClient.kt => GitHubClient.kt} (82%) rename src/main/kotlin/net/leanix/githubagent/config/{GithubEnterpriseProperties.kt => GitHubEnterpriseProperties.kt} (86%) rename src/main/kotlin/net/leanix/githubagent/dto/{GithubAppResponse.kt => GitHubAppResponse.kt} (92%) rename src/main/kotlin/net/leanix/githubagent/services/{GithubAuthenticationService.kt => GitHubAuthenticationService.kt} (93%) rename src/test/kotlin/net/leanix/githubagent/{GithubAgentApplicationTests.kt => GitHubAgentApplicationTests.kt} (88%) rename src/test/kotlin/net/leanix/githubagent/services/{GithubAuthenticationServiceTest.kt => GitHubAuthenticationServiceTest.kt} (86%) diff --git a/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt b/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt similarity index 79% rename from src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt rename to src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt index 54ca4dc..ec3f9fd 100644 --- a/src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt +++ b/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt @@ -7,9 +7,9 @@ import org.springframework.cloud.openfeign.EnableFeignClients @SpringBootApplication @EnableFeignClients(value = ["net.leanix.githubagent.client"]) -@EnableConfigurationProperties(value = [net.leanix.githubagent.config.GithubEnterpriseProperties::class]) -class GithubAgentApplication +@EnableConfigurationProperties(value = [net.leanix.githubagent.config.GitHubEnterpriseProperties::class]) +class GitHubAgentApplication fun main() { - runApplication() + runApplication() } diff --git a/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt b/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt similarity index 82% rename from src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt rename to src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt index 437afcd..36da759 100644 --- a/src/main/kotlin/net/leanix/githubagent/client/GithubClient.kt +++ b/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt @@ -1,16 +1,16 @@ package net.leanix.githubagent.client -import net.leanix.githubagent.dto.GithubAppResponse +import net.leanix.githubagent.dto.GitHubAppResponse import org.springframework.cloud.openfeign.FeignClient import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestHeader @FeignClient(name = "githubClient", url = "\${github-enterprise.baseUrl}") -interface GithubClient { +interface GitHubClient { @GetMapping("/api/v3/app") fun getApp( @RequestHeader("Authorization") jwt: String, @RequestHeader("Accept") accept: String = "application/vnd.github.v3+json" - ): GithubAppResponse + ): GitHubAppResponse } diff --git a/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt b/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt index f10e5c9..ef85fc6 100644 --- a/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt +++ b/src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt @@ -1,12 +1,12 @@ package net.leanix.githubagent.config import jakarta.annotation.PostConstruct -import net.leanix.githubagent.exceptions.GithubEnterpriseConfigurationMissingException +import net.leanix.githubagent.exceptions.GitHubEnterpriseConfigurationMissingException import org.springframework.stereotype.Component @Component class AgentSetupValidation( - private val githubEnterpriseProperties: GithubEnterpriseProperties + private val githubEnterpriseProperties: GitHubEnterpriseProperties ) { @PostConstruct @@ -24,7 +24,7 @@ class AgentSetupValidation( } if (missingProperties.isNotEmpty()) { - throw GithubEnterpriseConfigurationMissingException(missingProperties.joinToString(", ")) + throw GitHubEnterpriseConfigurationMissingException(missingProperties.joinToString(", ")) } } } diff --git a/src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt b/src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt similarity index 86% rename from src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt rename to src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt index 6dae6e3..7791c9e 100644 --- a/src/main/kotlin/net/leanix/githubagent/config/GithubEnterpriseProperties.kt +++ b/src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt @@ -3,7 +3,7 @@ package net.leanix.githubagent.config import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "github-enterprise") -data class GithubEnterpriseProperties( +data class GitHubEnterpriseProperties( val baseUrl: String, val githubAppId: String, val pemFile: String, diff --git a/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt b/src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt similarity index 92% rename from src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt rename to src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt index 9880aee..167859f 100644 --- a/src/main/kotlin/net/leanix/githubagent/dto/GithubAppResponse.kt +++ b/src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty @JsonIgnoreProperties(ignoreUnknown = true) -data class GithubAppResponse( +data class GitHubAppResponse( @JsonProperty("name") val name: String, @JsonProperty("permissions") val permissions: Map, @JsonProperty("events") val events: List diff --git a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt index 20bd154..a0b4691 100644 --- a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt +++ b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt @@ -1,8 +1,8 @@ package net.leanix.githubagent.exceptions -class GithubEnterpriseConfigurationMissingException(properties: String) : RuntimeException( +class GitHubEnterpriseConfigurationMissingException(properties: String) : RuntimeException( "Github Enterprise properties '$properties' are not set" ) -class GithubAppInsufficientPermissionsException(message: String) : RuntimeException(message) +class GitHubAppInsufficientPermissionsException(message: String) : RuntimeException(message) class FailedToCreateJWTException(message: String) : RuntimeException(message) -class UnableToConnectToGithubEnterpriseException(message: String) : RuntimeException(message) +class UnableToConnectToGitHubEnterpriseException(message: String) : RuntimeException(message) diff --git a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt index 08bdce2..897b19b 100644 --- a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt +++ b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt @@ -1,6 +1,6 @@ package net.leanix.githubagent.runners -import net.leanix.githubagent.services.GithubAuthenticationService +import net.leanix.githubagent.services.GitHubAuthenticationService import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner import org.springframework.context.annotation.Profile @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component @Component @Profile("!test") -class PostStartupRunner(private val githubAuthenticationService: GithubAuthenticationService) : ApplicationRunner { +class PostStartupRunner(private val githubAuthenticationService: GitHubAuthenticationService) : ApplicationRunner { override fun run(args: ApplicationArguments?) { githubAuthenticationService.generateJwtToken() diff --git a/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt index 24f9c3b..26566e6 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt @@ -1,12 +1,12 @@ package net.leanix.githubagent.services import jakarta.annotation.PostConstruct -import net.leanix.githubagent.config.GithubEnterpriseProperties +import net.leanix.githubagent.config.GitHubEnterpriseProperties import org.springframework.stereotype.Service @Service class CachingService( - private val githubEnterpriseProperties: GithubEnterpriseProperties + private val githubEnterpriseProperties: GitHubEnterpriseProperties ) { private val cache = HashMap() diff --git a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt similarity index 93% rename from src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt rename to src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt index 72a7f9d..c621bd8 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GithubAuthenticationService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt @@ -2,7 +2,7 @@ package net.leanix.githubagent.services import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm -import net.leanix.githubagent.config.GithubEnterpriseProperties +import net.leanix.githubagent.config.GitHubEnterpriseProperties import net.leanix.githubagent.exceptions.FailedToCreateJWTException import org.bouncycastle.jce.provider.BouncyCastleProvider import org.slf4j.LoggerFactory @@ -20,9 +20,9 @@ import java.security.spec.PKCS8EncodedKeySpec import java.util.* @Service -class GithubAuthenticationService( +class GitHubAuthenticationService( private val cachingService: CachingService, - private val githubEnterpriseProperties: GithubEnterpriseProperties, + private val githubEnterpriseProperties: GitHubEnterpriseProperties, private val resourceLoader: ResourceLoader, private val gitHubEnterpriseService: GitHubEnterpriseService ) { @@ -31,7 +31,7 @@ class GithubAuthenticationService( private const val JWT_EXPIRATION_DURATION = 600000L private const val pemPrefix = "-----BEGIN RSA PRIVATE KEY-----" private const val pemSuffix = "-----END RSA PRIVATE KEY-----" - private val logger = LoggerFactory.getLogger(GithubAuthenticationService::class.java) + private val logger = LoggerFactory.getLogger(GitHubAuthenticationService::class.java) } fun generateJwtToken() { diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt index 418acdf..d809db3 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt @@ -1,14 +1,14 @@ package net.leanix.githubagent.services -import net.leanix.githubagent.client.GithubClient -import net.leanix.githubagent.dto.GithubAppResponse -import net.leanix.githubagent.exceptions.GithubAppInsufficientPermissionsException -import net.leanix.githubagent.exceptions.UnableToConnectToGithubEnterpriseException +import net.leanix.githubagent.client.GitHubClient +import net.leanix.githubagent.dto.GitHubAppResponse +import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException +import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @Service -class GitHubEnterpriseService(private val githubClient: GithubClient) { +class GitHubEnterpriseService(private val githubClient: GitHubClient) { companion object { val expectedPermissions = listOf("administration", "contents", "metadata") @@ -22,14 +22,15 @@ class GitHubEnterpriseService(private val githubClient: GithubClient) { validateGithubAppResponse(githubApp) logger.info("Authenticated as GitHub App: '${githubApp.name}'") }.onFailure { + logger.error("Failed to verify JWT token", it) when (it) { - is GithubAppInsufficientPermissionsException -> throw it - else -> throw UnableToConnectToGithubEnterpriseException("Failed to verify JWT token") + is GitHubAppInsufficientPermissionsException -> throw it + else -> throw UnableToConnectToGitHubEnterpriseException("Failed to verify JWT token") } } } - fun validateGithubAppResponse(response: GithubAppResponse) { + fun validateGithubAppResponse(response: GitHubAppResponse) { val missingPermissions = expectedPermissions.filterNot { response.permissions.containsKey(it) } val missingEvents = expectedEvents.filterNot { response.events.contains(it) } @@ -44,7 +45,7 @@ class GitHubEnterpriseService(private val githubClient: GithubClient) { } message = message.plus("events: $missingEvents") } - throw GithubAppInsufficientPermissionsException(message) + throw GitHubAppInsufficientPermissionsException(message) } } } diff --git a/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt b/src/test/kotlin/net/leanix/githubagent/GitHubAgentApplicationTests.kt similarity index 88% rename from src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt rename to src/test/kotlin/net/leanix/githubagent/GitHubAgentApplicationTests.kt index 07458a3..2405e95 100644 --- a/src/test/kotlin/net/leanix/githubagent/GithubAgentApplicationTests.kt +++ b/src/test/kotlin/net/leanix/githubagent/GitHubAgentApplicationTests.kt @@ -6,7 +6,7 @@ import org.springframework.test.context.ActiveProfiles @SpringBootTest @ActiveProfiles("test") -class GithubAgentApplicationTests { +class GitHubAgentApplicationTests { @Test fun contextLoads() { diff --git a/src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt similarity index 86% rename from src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt rename to src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt index 80659b3..be6fbde 100644 --- a/src/test/kotlin/net/leanix/githubagent/services/GithubAuthenticationServiceTest.kt +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt @@ -1,9 +1,9 @@ import io.mockk.every import io.mockk.mockk -import net.leanix.githubagent.config.GithubEnterpriseProperties +import net.leanix.githubagent.config.GitHubEnterpriseProperties import net.leanix.githubagent.services.CachingService +import net.leanix.githubagent.services.GitHubAuthenticationService import net.leanix.githubagent.services.GitHubEnterpriseService -import net.leanix.githubagent.services.GithubAuthenticationService import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test @@ -11,13 +11,13 @@ import org.junit.jupiter.api.assertDoesNotThrow import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ResourceLoader -class GithubAuthenticationServiceTest { +class GitHubAuthenticationServiceTest { private val cachingService = mockk() - private val githubEnterpriseProperties = mockk() + private val githubEnterpriseProperties = mockk() private val resourceLoader = mockk() private val gitHubEnterpriseService = mockk() - private val githubAuthenticationService = GithubAuthenticationService( + private val githubAuthenticationService = GitHubAuthenticationService( cachingService, githubEnterpriseProperties, resourceLoader, diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt index 92c88a7..7e4fdf5 100644 --- a/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt @@ -1,9 +1,9 @@ import io.mockk.every import io.mockk.mockk -import net.leanix.githubagent.client.GithubClient -import net.leanix.githubagent.dto.GithubAppResponse -import net.leanix.githubagent.exceptions.GithubAppInsufficientPermissionsException -import net.leanix.githubagent.exceptions.UnableToConnectToGithubEnterpriseException +import net.leanix.githubagent.client.GitHubClient +import net.leanix.githubagent.dto.GitHubAppResponse +import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException +import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException import net.leanix.githubagent.services.GitHubEnterpriseService import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test @@ -11,13 +11,13 @@ import org.junit.jupiter.api.assertDoesNotThrow class GitHubEnterpriseServiceTest { - private val githubClient = mockk() + private val githubClient = mockk() private val service = GitHubEnterpriseService(githubClient) @Test fun `verifyJwt with valid jwt should not throw exception`() { val jwt = "validJwt" - val githubApp = GithubAppResponse( + val githubApp = GitHubAppResponse( name = "validApp", permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), events = listOf("label", "public", "repository") @@ -32,12 +32,12 @@ class GitHubEnterpriseServiceTest { val jwt = "invalidJwt" every { githubClient.getApp(any()) } throws Exception() - assertThrows(UnableToConnectToGithubEnterpriseException::class.java) { service.verifyJwt(jwt) } + assertThrows(UnableToConnectToGitHubEnterpriseException::class.java) { service.verifyJwt(jwt) } } @Test fun `validateGithubAppResponse with correct permissions should not throw exception`() { - val response = GithubAppResponse( + val response = GitHubAppResponse( name = "validApp", permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), events = listOf("label", "public", "repository") @@ -48,27 +48,27 @@ class GitHubEnterpriseServiceTest { @Test fun `validateGithubAppResponse with missing permissions should throw exception`() { - val response = GithubAppResponse( + val response = GitHubAppResponse( name = "validApp", permissions = mapOf("administration" to "read", "contents" to "read"), events = listOf("label", "public", "repository") ) assertThrows( - GithubAppInsufficientPermissionsException::class.java + GitHubAppInsufficientPermissionsException::class.java ) { service.validateGithubAppResponse(response) } } @Test fun `validateGithubAppResponse with missing events should throw exception`() { - val response = GithubAppResponse( + val response = GitHubAppResponse( name = "validApp", permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read"), events = listOf("label", "public") ) assertThrows( - GithubAppInsufficientPermissionsException::class.java + GitHubAppInsufficientPermissionsException::class.java ) { service.validateGithubAppResponse(response) } } } From 53a164ac050eb1b9fe2736d27e17cc2749e4c6df Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Fri, 5 Jul 2024 09:24:49 +0200 Subject: [PATCH 5/6] CID-2732: Address PR comments --- .../kotlin/net/leanix/githubagent/GitHubAgentApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt b/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt index ec3f9fd..2d6cf17 100644 --- a/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt +++ b/src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt @@ -6,7 +6,7 @@ import org.springframework.boot.runApplication import org.springframework.cloud.openfeign.EnableFeignClients @SpringBootApplication -@EnableFeignClients(value = ["net.leanix.githubagent.client"]) +@EnableFeignClients @EnableConfigurationProperties(value = [net.leanix.githubagent.config.GitHubEnterpriseProperties::class]) class GitHubAgentApplication From f0f6d6440d1865f4b360371134492356a14a81dc Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Fri, 5 Jul 2024 10:44:17 +0200 Subject: [PATCH 6/6] CID-2732: use spring provided cache --- build.gradle.kts | 2 + .../githubagent/services/CachingService.kt | 53 +++++++++++++++---- .../services/GitHubAuthenticationService.kt | 4 +- .../GitHubAuthenticationServiceTest.kt | 2 +- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7cc88ac..d71accb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,8 @@ dependencyManagement { dependencies { implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-cache") + implementation("com.github.ben-manes.caffeine:caffeine:2.8.8") implementation("org.springframework.cloud:spring-cloud-starter-openfeign") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("com.fasterxml.jackson.core:jackson-annotations:2.17.1") diff --git a/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt index 26566e6..900934b 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/CachingService.kt @@ -1,5 +1,8 @@ package net.leanix.githubagent.services +import com.github.benmanes.caffeine.cache.Cache +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Expiry import jakarta.annotation.PostConstruct import net.leanix.githubagent.config.GitHubEnterpriseProperties import org.springframework.stereotype.Service @@ -8,19 +11,51 @@ import org.springframework.stereotype.Service class CachingService( private val githubEnterpriseProperties: GitHubEnterpriseProperties ) { - private val cache = HashMap() - @PostConstruct - private fun init() { - cache["baseUrl"] = githubEnterpriseProperties.baseUrl - cache["githubAppId"] = githubEnterpriseProperties.githubAppId + data class CacheValue(val value: Any, val expiry: Long?) + + private val cache: Cache = Caffeine.newBuilder() + .maximumSize(100) + .expireAfter(object : Expiry { + override fun expireAfterCreate( + key: String, + value: CacheValue, + currentTime: Long + ): Long { + return value.expiry ?: Long.MAX_VALUE + } + + override fun expireAfterUpdate( + key: String, + value: CacheValue, + currentTime: Long, + currentDuration: Long + ): Long { + return value.expiry ?: Long.MAX_VALUE + } + + override fun expireAfterRead( + key: String, + value: CacheValue, + currentTime: Long, + currentDuration: Long + ): Long { + return currentDuration + } + }) + .build() + + fun set(key: String, value: Any, expiry: Long?) { + cache.put(key, CacheValue(value, expiry)) } - fun set(key: String, value: String) { - cache[key] = value + fun get(key: String): Any? { + return cache.getIfPresent(key)?.value } - fun get(key: String): String? { - return cache[key] + @PostConstruct + private fun init() { + set("baseUrl", githubEnterpriseProperties.baseUrl, null) + set("githubAppId", githubEnterpriseProperties.githubAppId, null) } } diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt index c621bd8..c76c573 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt @@ -42,7 +42,7 @@ class GitHubAuthenticationService( val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey)) val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec) val jwt = createJwtToken(privateKey) - cachingService.set("jwtToken", jwt.getOrThrow()) + cachingService.set("jwtToken", jwt.getOrThrow(), JWT_EXPIRATION_DURATION) gitHubEnterpriseService.verifyJwt(jwt.getOrThrow()) }.onFailure { logger.error("Failed to generate/validate JWT token", it) @@ -59,7 +59,7 @@ class GitHubAuthenticationService( Jwts.builder() .setIssuedAt(Date()) .setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION)) - .setIssuer(cachingService.get("githubAppId")) + .setIssuer(cachingService.get("githubAppId").toString()) .signWith(privateKey, SignatureAlgorithm.RS256) .compact() }.onFailure { diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt index be6fbde..23b6882 100644 --- a/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt @@ -27,7 +27,7 @@ class GitHubAuthenticationServiceTest { @Test fun `generateJwtToken with valid data should not throw exception`() { every { cachingService.get(any()) } returns "dummy-value" - every { cachingService.set(any(), any()) } returns Unit + every { cachingService.set(any(), any(), any()) } returns Unit every { githubEnterpriseProperties.pemFile } returns "valid-private-key.pem" every { resourceLoader.getResource(any()) } returns ClassPathResource("valid-private-key.pem") every { gitHubEnterpriseService.verifyJwt(any()) } returns Unit