generated from leanix/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from leanix/feature/CID-2732/authenticate-to-ghe
Feature/cid 2732/authenticate to ghe
- Loading branch information
Showing
19 changed files
with
517 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
src/main/kotlin/net/leanix/githubagent/GitHubAgentApplication.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
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 | ||
@EnableConfigurationProperties(value = [net.leanix.githubagent.config.GitHubEnterpriseProperties::class]) | ||
class GitHubAgentApplication | ||
|
||
fun main() { | ||
runApplication<GitHubAgentApplication>() | ||
} |
11 changes: 0 additions & 11 deletions
11
src/main/kotlin/net/leanix/githubagent/GithubAgentApplication.kt
This file was deleted.
Oops, something went wrong.
16 changes: 16 additions & 0 deletions
16
src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
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 | ||
|
||
@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" | ||
): GitHubAppResponse | ||
} |
30 changes: 30 additions & 0 deletions
30
src/main/kotlin/net/leanix/githubagent/config/AgentSetupValidation.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String>() | ||
|
||
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(", ")) | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/kotlin/net/leanix/githubagent/config/GitHubEnterpriseProperties.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |
11 changes: 11 additions & 0 deletions
11
src/main/kotlin/net/leanix/githubagent/dto/GitHubAppResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
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, | ||
@JsonProperty("permissions") val permissions: Map<String, String>, | ||
@JsonProperty("events") val events: List<String> | ||
) |
8 changes: 8 additions & 0 deletions
8
src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package net.leanix.githubagent.exceptions | ||
|
||
class GitHubEnterpriseConfigurationMissingException(properties: String) : RuntimeException( | ||
"Github Enterprise properties '$properties' are not set" | ||
) | ||
class GitHubAppInsufficientPermissionsException(message: String) : RuntimeException(message) | ||
class FailedToCreateJWTException(message: String) : RuntimeException(message) | ||
class UnableToConnectToGitHubEnterpriseException(message: String) : RuntimeException(message) |
16 changes: 16 additions & 0 deletions
16
src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
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?) { | ||
githubAuthenticationService.generateJwtToken() | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
src/main/kotlin/net/leanix/githubagent/services/CachingService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
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 | ||
|
||
@Service | ||
class CachingService( | ||
private val githubEnterpriseProperties: GitHubEnterpriseProperties | ||
) { | ||
|
||
data class CacheValue(val value: Any, val expiry: Long?) | ||
|
||
private val cache: Cache<String, CacheValue> = Caffeine.newBuilder() | ||
.maximumSize(100) | ||
.expireAfter(object : Expiry<String, CacheValue> { | ||
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 get(key: String): Any? { | ||
return cache.getIfPresent(key)?.value | ||
} | ||
|
||
@PostConstruct | ||
private fun init() { | ||
set("baseUrl", githubEnterpriseProperties.baseUrl, null) | ||
set("githubAppId", githubEnterpriseProperties.githubAppId, null) | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package net.leanix.githubagent.services | ||
|
||
import io.jsonwebtoken.Jwts | ||
import io.jsonwebtoken.SignatureAlgorithm | ||
import net.leanix.githubagent.config.GitHubEnterpriseProperties | ||
import net.leanix.githubagent.exceptions.FailedToCreateJWTException | ||
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.InvalidKeySpecException | ||
import java.security.spec.PKCS8EncodedKeySpec | ||
import java.util.* | ||
|
||
@Service | ||
class GitHubAuthenticationService( | ||
private val cachingService: CachingService, | ||
private val githubEnterpriseProperties: GitHubEnterpriseProperties, | ||
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) | ||
} | ||
|
||
fun generateJwtToken() { | ||
runCatching { | ||
logger.info("Generating JWT token") | ||
Security.addProvider(BouncyCastleProvider()) | ||
val rsaPrivateKey: String = readPrivateKey() | ||
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey)) | ||
val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec) | ||
val jwt = createJwtToken(privateKey) | ||
cachingService.set("jwtToken", jwt.getOrThrow(), JWT_EXPIRATION_DURATION) | ||
gitHubEnterpriseService.verifyJwt(jwt.getOrThrow()) | ||
}.onFailure { | ||
logger.error("Failed to generate/validate JWT token", it) | ||
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): Result<String> { | ||
return runCatching { | ||
Jwts.builder() | ||
.setIssuedAt(Date()) | ||
.setExpiration(Date(System.currentTimeMillis() + JWT_EXPIRATION_DURATION)) | ||
.setIssuer(cachingService.get("githubAppId").toString()) | ||
.signWith(privateKey, SignatureAlgorithm.RS256) | ||
.compact() | ||
}.onFailure { | ||
throw FailedToCreateJWTException("Failed to generate a valid JWT token") | ||
} | ||
} | ||
|
||
@Throws(IOException::class) | ||
private fun readPrivateKey(): String { | ||
val pemFile = File(resourceLoader.getResource("file:${githubEnterpriseProperties.pemFile}").uri) | ||
val fileContent = String(Files.readAllBytes(pemFile.toPath()), Charset.defaultCharset()).trim() | ||
|
||
require(fileContent.startsWith(pemPrefix) && fileContent.endsWith(pemSuffix)) { | ||
"The provided file is not a valid PEM file." | ||
} | ||
|
||
return fileContent | ||
.replace(pemPrefix, "") | ||
.replace(System.lineSeparator().toRegex(), "") | ||
.replace(pemSuffix, "") | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
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 { | ||
logger.error("Failed to verify JWT token", it) | ||
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
github-enterprise: | ||
baseUrl: ${GITHUB_ENTERPRISE_BASE_URL:} | ||
githubAppId: ${GITHUB_APP_ID:} | ||
pemFile: ${PEM_FILE:} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.