From 889cb63a1925e493775d47aaac1eac400f6be587 Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Fri, 12 Jul 2024 10:50:07 +0200 Subject: [PATCH 1/5] CID-2744: initial commit for getting organizations --- .../leanix/githubagent/client/GitHubClient.kt | 24 +++++++++ .../githubagent/dto/GitHubResponsesDto.kt | 38 ++++++++++++++ .../leanix/githubagent/dto/OrganizationDto.kt | 7 +++ .../githubagent/exceptions/Exceptions.kt | 1 + .../githubagent/runners/PostStartupRunner.kt | 5 +- .../services/GitHubScanningService.kt | 51 +++++++++++++++++++ 6 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/net/leanix/githubagent/dto/GitHubResponsesDto.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/dto/OrganizationDto.kt create mode 100644 src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt diff --git a/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt b/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt index 36da759..c69e1e4 100644 --- a/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt +++ b/src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt @@ -1,8 +1,14 @@ package net.leanix.githubagent.client import net.leanix.githubagent.dto.GitHubAppResponse +import net.leanix.githubagent.dto.Installation +import net.leanix.githubagent.dto.InstallationTokenResponse +import net.leanix.githubagent.dto.Organization +import net.leanix.githubagent.dto.Repository import org.springframework.cloud.openfeign.FeignClient import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestHeader @FeignClient(name = "githubClient", url = "\${github-enterprise.baseUrl}") @@ -13,4 +19,22 @@ interface GitHubClient { @RequestHeader("Authorization") jwt: String, @RequestHeader("Accept") accept: String = "application/vnd.github.v3+json" ): GitHubAppResponse + + @GetMapping("/api/v3/app/installations") + fun getInstallations(@RequestHeader("Authorization") jwt: String): List + + @PostMapping("/api/v3/app/installations/{installationId}/access_tokens") + fun createInstallationToken( + @PathVariable("installationId") installationId: Long, + @RequestHeader("Authorization") jwt: String + ): InstallationTokenResponse + + @GetMapping("/api/v3/organizations") + fun getOrganizations(@RequestHeader("Authorization") token: String): List + + @GetMapping("/api/v3/orgs/{org}/repos") + fun getRepositories( + @PathVariable("org") org: String, + @RequestHeader("Authorization") token: String + ): List } diff --git a/src/main/kotlin/net/leanix/githubagent/dto/GitHubResponsesDto.kt b/src/main/kotlin/net/leanix/githubagent/dto/GitHubResponsesDto.kt new file mode 100644 index 0000000..8196e70 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/dto/GitHubResponsesDto.kt @@ -0,0 +1,38 @@ +package net.leanix.githubagent.dto + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +data class InstallationTokenResponse( + @JsonProperty("token") val token: String, + @JsonProperty("expires_at") val expiresAt: String, + @JsonProperty("permissions") val permissions: Map, + @JsonProperty("repository_selection") val repositorySelection: String +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Installation( + @JsonProperty("id") val id: Long, + @JsonProperty("account") val account: Account +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Account( + @JsonProperty("login") val login: String +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Organization( + @JsonProperty("login") val login: String, + @JsonProperty("id") val id: Int, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Repository( + @JsonProperty("id") val id: String, + @JsonProperty("name") val name: String, + @JsonProperty("full_name") val fullName: Boolean, + @JsonProperty("owner") val owner: Organization, + @JsonProperty("topics") val topics: List +) diff --git a/src/main/kotlin/net/leanix/githubagent/dto/OrganizationDto.kt b/src/main/kotlin/net/leanix/githubagent/dto/OrganizationDto.kt new file mode 100644 index 0000000..4702a06 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/dto/OrganizationDto.kt @@ -0,0 +1,7 @@ +package net.leanix.githubagent.dto + +class OrganizationDto( + val id: Int, + val name: String, + val installed: Boolean +) diff --git a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt index a0b4691..e790bdc 100644 --- a/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt +++ b/src/main/kotlin/net/leanix/githubagent/exceptions/Exceptions.kt @@ -6,3 +6,4 @@ class GitHubEnterpriseConfigurationMissingException(properties: String) : Runtim class GitHubAppInsufficientPermissionsException(message: String) : RuntimeException(message) class FailedToCreateJWTException(message: String) : RuntimeException(message) class UnableToConnectToGitHubEnterpriseException(message: String) : RuntimeException(message) +class JwtTokenNotFound : RuntimeException("JWT token not found") diff --git a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt index 9c236be..bcbfc47 100644 --- a/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt +++ b/src/main/kotlin/net/leanix/githubagent/runners/PostStartupRunner.kt @@ -1,6 +1,7 @@ package net.leanix.githubagent.runners import net.leanix.githubagent.services.GitHubAuthenticationService +import net.leanix.githubagent.services.GitHubScanningService import net.leanix.githubagent.services.WebSocketService import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner @@ -11,11 +12,13 @@ import org.springframework.stereotype.Component @Profile("!test") class PostStartupRunner( private val githubAuthenticationService: GitHubAuthenticationService, - private val webSocketService: WebSocketService + private val webSocketService: WebSocketService, + private val gitHubScanningService: GitHubScanningService ) : ApplicationRunner { override fun run(args: ApplicationArguments?) { githubAuthenticationService.generateJwtToken() webSocketService.initSession() + gitHubScanningService.scanGitHubResources() } } diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt new file mode 100644 index 0000000..e93081b --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt @@ -0,0 +1,51 @@ +package net.leanix.githubagent.services + +import net.leanix.githubagent.client.GitHubClient +import net.leanix.githubagent.dto.Installation +import net.leanix.githubagent.dto.OrganizationDto +import net.leanix.githubagent.exceptions.JwtTokenNotFound +import org.springframework.stereotype.Service + +@Service +class GitHubScanningService( + private val gitHubClient: GitHubClient, + private val cachingService: CachingService, +) { + fun scanGitHubResources() { + val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound() + val installations = getInstallations(jwtToken.toString()) + generateOrganizations(installations) + // send organizations to backend + } + + private fun getInstallations(jwtToken: String): List { + val installations = gitHubClient.getInstallations("Bearer $jwtToken") + generateAndCacheInstallationTokens(installations, jwtToken) + return installations + } + + private fun generateAndCacheInstallationTokens( + installations: List, + jwtToken: String + ) { + installations.forEach { installation -> + val installationToken = gitHubClient.createInstallationToken(installation.id, "Bearer $jwtToken").token + cachingService.set("installationToken:${installation.id}", installationToken, null) + // cachingService.set("installationToken:${installation.id}", installationToken, 3600L) + } + } + + private fun generateOrganizations( + installations: List + ): List { + val installationToken = cachingService.get("installationToken:${installations.first().id}") + val organizations = gitHubClient.getOrganizations("Bearer $installationToken") + return organizations.map { organization -> + if (installations.find { it.account.login == organization.login } != null) { + OrganizationDto(organization.id, organization.login, true) + } else { + OrganizationDto(organization.id, organization.login, false) + } + } + } +} From 19cd1e311c08456f736cc5e6f5c57e2086aeef74 Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Fri, 12 Jul 2024 10:51:47 +0200 Subject: [PATCH 2/5] CID-2744: set installation token expiry --- .../net/leanix/githubagent/services/GitHubScanningService.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt index e93081b..be72f0f 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt @@ -30,8 +30,7 @@ class GitHubScanningService( ) { installations.forEach { installation -> val installationToken = gitHubClient.createInstallationToken(installation.id, "Bearer $jwtToken").token - cachingService.set("installationToken:${installation.id}", installationToken, null) - // cachingService.set("installationToken:${installation.id}", installationToken, 3600L) + cachingService.set("installationToken:${installation.id}", installationToken, 3600L) } } From 3feb54461096aa3aa725fa8f91434fe8ed7ba519 Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Tue, 16 Jul 2024 13:40:33 +0200 Subject: [PATCH 3/5] CID-2744: send list of organizations to backend --- .../net/leanix/githubagent/services/GitHubScanningService.kt | 5 +++-- .../net/leanix/githubagent/services/WebSocketService.kt | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt index be72f0f..0b269dc 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt @@ -10,12 +10,13 @@ import org.springframework.stereotype.Service class GitHubScanningService( private val gitHubClient: GitHubClient, private val cachingService: CachingService, + private val webSocketService: WebSocketService ) { fun scanGitHubResources() { val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound() val installations = getInstallations(jwtToken.toString()) - generateOrganizations(installations) - // send organizations to backend + val organizations = generateOrganizations(installations) + webSocketService.sendMessage("/app/ghe/organizations", organizations) } private fun getInstallations(jwtToken: String): List { diff --git a/src/main/kotlin/net/leanix/githubagent/services/WebSocketService.kt b/src/main/kotlin/net/leanix/githubagent/services/WebSocketService.kt index 7f9e551..8959113 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/WebSocketService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/WebSocketService.kt @@ -17,4 +17,8 @@ class WebSocketService( logger.info("init session") stompSession = webSocketClientConfig.initSession() } + + fun sendMessage(topic: String, data: Any) { + stompSession!!.send(topic, data) + } } From 25c722ea90a2fa1417375c1a793f703390a50254 Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Tue, 16 Jul 2024 16:18:25 +0200 Subject: [PATCH 4/5] CID-2744: test for GitHubScanningService --- .../services/GitHubScanningServiceTest.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt new file mode 100644 index 0000000..96d7bca --- /dev/null +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt @@ -0,0 +1,48 @@ +package net.leanix.githubagent.services + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import net.leanix.githubagent.client.GitHubClient +import net.leanix.githubagent.dto.Account +import net.leanix.githubagent.dto.Installation +import net.leanix.githubagent.dto.InstallationTokenResponse +import net.leanix.githubagent.dto.Organization +import net.leanix.githubagent.exceptions.JwtTokenNotFound +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class GitHubScanningServiceTest { + + private val gitHubClient = mockk() + private val cachingService = mockk() + private val webSocketService = mockk(relaxUnitFun = true) + private val gitHubScanningService = GitHubScanningService(gitHubClient, cachingService, webSocketService) + + @BeforeEach + fun setup() { + every { cachingService.get(any()) } returns "value" + every { gitHubClient.getInstallations(any()) } returns listOf( + Installation(1, Account("testInstallation")) + ) + every { gitHubClient.createInstallationToken(1, any()) } returns + InstallationTokenResponse("testToken", "2024-01-01T00:00:00Z", mapOf(), "all") + every { cachingService.set(any(), any(), any()) } returns Unit + every { gitHubClient.getOrganizations(any()) } returns listOf(Organization("testOrganization", 1)) + } + + @Test + fun `scanGitHubResources should send organizations over WebSocket`() { + gitHubScanningService.scanGitHubResources() + verify { webSocketService.sendMessage("/app/ghe/organizations", any()) } + } + + @Test + fun `scanGitHubResources should throw JwtTokenNotFound when jwtToken is expired`() { + every { cachingService.get("jwtToken") } returns null + assertThrows { + gitHubScanningService.scanGitHubResources() + } + } +} From 978a26aa26d6ebbe04cdcbaf6358fc6737f846ac Mon Sep 17 00:00:00 2001 From: mohamedlajmileanix Date: Wed, 17 Jul 2024 10:04:13 +0200 Subject: [PATCH 5/5] CID-2744: addressing PR comments, add a log statement on scan failure --- .../services/GitHubScanningService.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt index 0b269dc..cbaab74 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt @@ -4,6 +4,7 @@ import net.leanix.githubagent.client.GitHubClient import net.leanix.githubagent.dto.Installation import net.leanix.githubagent.dto.OrganizationDto import net.leanix.githubagent.exceptions.JwtTokenNotFound +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @Service @@ -12,11 +13,18 @@ class GitHubScanningService( private val cachingService: CachingService, private val webSocketService: WebSocketService ) { + private val logger = LoggerFactory.getLogger(GitHubScanningService::class.java) + fun scanGitHubResources() { - val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound() - val installations = getInstallations(jwtToken.toString()) - val organizations = generateOrganizations(installations) - webSocketService.sendMessage("/app/ghe/organizations", organizations) + runCatching { + val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound() + val installations = getInstallations(jwtToken.toString()) + val organizations = generateOrganizations(installations) + webSocketService.sendMessage("/app/ghe/organizations", organizations) + }.onFailure { + logger.error("Error while scanning GitHub resources") + throw it + } } private fun getInstallations(jwtToken: String): List {