Skip to content

Commit 5ca9182

Browse files
CID-2911: merge main
2 parents 7f56f31 + b793587 commit 5ca9182

File tree

13 files changed

+334
-44
lines changed

13 files changed

+334
-44
lines changed

src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,17 @@ interface GitHubClient {
2828
): GitHubAppResponse
2929

3030
@GetMapping("/api/v3/app/installations")
31-
fun getInstallations(@RequestHeader("Authorization") jwt: String): List<Installation>
31+
fun getInstallations(
32+
@RequestHeader("Authorization") jwt: String,
33+
@RequestParam("per_page", defaultValue = "30") perPage: Int,
34+
@RequestParam("page", defaultValue = "1") page: Int
35+
): List<Installation>
36+
37+
@GetMapping("/api/v3/app/installations/{installationId}")
38+
fun getInstallation(
39+
@PathVariable("installationId") installationId: Long,
40+
@RequestHeader("Authorization") jwt: String
41+
): Installation
3242

3343
@PostMapping("/api/v3/app/installations/{installationId}/access_tokens")
3444
fun createInstallationToken(
@@ -37,7 +47,11 @@ interface GitHubClient {
3747
): InstallationTokenResponse
3848

3949
@GetMapping("/api/v3/organizations")
40-
fun getOrganizations(@RequestHeader("Authorization") token: String): List<Organization>
50+
fun getOrganizations(
51+
@RequestHeader("Authorization") jwt: String,
52+
@RequestParam("per_page", defaultValue = "30") perPage: Int,
53+
@RequestParam("since", defaultValue = "1") since: Int
54+
): List<Organization>
4155

4256
@GetMapping("/api/v3/orgs/{org}/repos")
4357
fun getRepositories(

src/main/kotlin/net/leanix/githubagent/dto/GitHubResponsesDto.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ data class InstallationTokenResponse(
1414
@JsonIgnoreProperties(ignoreUnknown = true)
1515
data class Installation(
1616
@JsonProperty("id") val id: Long,
17-
@JsonProperty("account") val account: Account
17+
@JsonProperty("account") val account: Account,
18+
@JsonProperty("permissions") val permissions: Map<String, String>,
19+
@JsonProperty("events") val events: List<String>
1820
)
1921

2022
@JsonIgnoreProperties(ignoreUnknown = true)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.leanix.githubagent.services
2+
3+
import net.leanix.githubagent.client.GitHubClient
4+
import net.leanix.githubagent.dto.Installation
5+
import net.leanix.githubagent.dto.Organization
6+
import org.springframework.stereotype.Service
7+
8+
@Service
9+
class GitHubAPIService(
10+
private val gitHubClient: GitHubClient,
11+
) {
12+
13+
companion object {
14+
private const val PAGE_SIZE = 30 // Maximum allowed by GitHub API is 100
15+
}
16+
17+
fun getPaginatedInstallations(jwtToken: String): List<Installation> {
18+
val installations = mutableListOf<Installation>()
19+
var page = 1
20+
var currentInstallations: List<Installation>
21+
22+
do {
23+
currentInstallations = gitHubClient.getInstallations("Bearer $jwtToken", PAGE_SIZE, page)
24+
if (currentInstallations.isNotEmpty()) installations.addAll(currentInstallations) else break
25+
page++
26+
} while (currentInstallations.size == PAGE_SIZE)
27+
return installations
28+
}
29+
30+
fun getPaginatedOrganizations(installationToken: String): List<Organization> {
31+
val organizations = mutableListOf<Organization>()
32+
var since = 1
33+
var currentOrganizations: List<Organization>
34+
35+
do {
36+
currentOrganizations = gitHubClient.getOrganizations("Bearer $installationToken", PAGE_SIZE, since)
37+
if (currentOrganizations.isNotEmpty()) organizations.addAll(currentOrganizations) else break
38+
since = currentOrganizations.last().id
39+
} while (currentOrganizations.size == PAGE_SIZE)
40+
return organizations
41+
}
42+
}

src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import net.leanix.githubagent.client.GitHubClient
66
import net.leanix.githubagent.config.GitHubEnterpriseProperties
77
import net.leanix.githubagent.dto.Installation
88
import net.leanix.githubagent.exceptions.FailedToCreateJWTException
9+
import net.leanix.githubagent.exceptions.JwtTokenNotFound
910
import org.bouncycastle.jce.provider.BouncyCastleProvider
1011
import org.slf4j.LoggerFactory
1112
import org.springframework.core.io.ResourceLoader
@@ -26,7 +27,8 @@ class GitHubAuthenticationService(
2627
private val githubEnterpriseProperties: GitHubEnterpriseProperties,
2728
private val resourceLoader: ResourceLoader,
2829
private val gitHubEnterpriseService: GitHubEnterpriseService,
29-
private val gitHubClient: GitHubClient
30+
private val gitHubClient: GitHubClient,
31+
private val gitHubAPIService: GitHubAPIService,
3032
) {
3133

3234
companion object {
@@ -38,11 +40,9 @@ class GitHubAuthenticationService(
3840

3941
fun refreshTokens() {
4042
generateAndCacheJwtToken()
41-
val jwtToken = cachingService.get("jwtToken")
42-
generateAndCacheInstallationTokens(
43-
gitHubClient.getInstallations("Bearer $jwtToken"),
44-
jwtToken.toString()
45-
)
43+
val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound()
44+
val installations = gitHubAPIService.getPaginatedInstallations(jwtToken.toString())
45+
generateAndCacheInstallationTokens(installations, jwtToken.toString())
4646
}
4747

4848
fun generateAndCacheJwtToken() {

src/main/kotlin/net/leanix/githubagent/services/GitHubEnterpriseService.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package net.leanix.githubagent.services
22

33
import net.leanix.githubagent.client.GitHubClient
4-
import net.leanix.githubagent.dto.GitHubAppResponse
54
import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException
65
import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException
6+
import net.leanix.githubagent.shared.GITHUB_APP_LABEL
77
import org.slf4j.LoggerFactory
88
import org.springframework.stereotype.Service
99

1010
@Service
11-
class GitHubEnterpriseService(private val githubClient: GitHubClient) {
11+
class GitHubEnterpriseService(
12+
private val githubClient: GitHubClient,
13+
private val syncLogService: SyncLogService,
14+
) {
1215

1316
companion object {
1417
val expectedPermissions = listOf("administration", "contents", "metadata")
@@ -19,29 +22,34 @@ class GitHubEnterpriseService(private val githubClient: GitHubClient) {
1922
fun verifyJwt(jwt: String) {
2023
runCatching {
2124
val githubApp = getGitHubApp(jwt)
22-
validateGithubAppResponse(githubApp)
25+
validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, githubApp.permissions, githubApp.events)
2326
logger.info("Authenticated as GitHub App: '${githubApp.slug}'")
2427
}.onFailure {
25-
logger.error("Failed to verify JWT token", it)
2628
when (it) {
27-
is GitHubAppInsufficientPermissionsException -> throw it
28-
else -> throw UnableToConnectToGitHubEnterpriseException("Failed to verify JWT token")
29+
is GitHubAppInsufficientPermissionsException -> {
30+
logger.error(it.message)
31+
syncLogService.sendErrorLog(it.message!!)
32+
}
33+
else -> {
34+
logger.error("Failed to verify JWT token", it)
35+
throw UnableToConnectToGitHubEnterpriseException("Failed to verify JWT token")
36+
}
2937
}
3038
}
3139
}
3240

33-
fun validateGithubAppResponse(response: GitHubAppResponse) {
34-
val missingPermissions = expectedPermissions.filterNot { response.permissions.containsKey(it) }
35-
val missingEvents = expectedEvents.filterNot { response.events.contains(it) }
41+
fun validateEnabledPermissionsAndEvents(type: String, permissions: Map<String, String>, events: List<String>) {
42+
val missingPermissions = expectedPermissions.filterNot { permissions.containsKey(it) }
43+
val missingEvents = expectedEvents.filterNot { events.contains(it) }
3644

3745
if (missingPermissions.isNotEmpty() || missingEvents.isNotEmpty()) {
38-
var message = "GitHub App is missing the following "
46+
var message = "$type missing the following "
3947
if (missingPermissions.isNotEmpty()) {
4048
message = message.plus("permissions: $missingPermissions")
4149
}
4250
if (missingEvents.isNotEmpty()) {
4351
if (missingPermissions.isNotEmpty()) {
44-
message = message.plus(", and the following")
52+
message = message.plus(", and the following ")
4553
}
4654
message = message.plus("events: $missingEvents")
4755
}

src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import net.leanix.githubagent.dto.Organization
99
import net.leanix.githubagent.dto.OrganizationDto
1010
import net.leanix.githubagent.dto.RateLimitType
1111
import net.leanix.githubagent.dto.RepositoryDto
12+
import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException
1213
import net.leanix.githubagent.exceptions.JwtTokenNotFound
1314
import net.leanix.githubagent.exceptions.ManifestFileNotFoundException
1415
import net.leanix.githubagent.handler.RateLimitHandler
16+
import net.leanix.githubagent.shared.INSTALLATION_LABEL
1517
import net.leanix.githubagent.shared.MANIFEST_FILE_NAME
1618
import net.leanix.githubagent.shared.fileNameMatchRegex
1719
import net.leanix.githubagent.shared.generateFullPath
@@ -27,6 +29,8 @@ class GitHubScanningService(
2729
private val gitHubAuthenticationService: GitHubAuthenticationService,
2830
private val syncLogService: SyncLogService,
2931
private val rateLimitHandler: RateLimitHandler,
32+
private val gitHubEnterpriseService: GitHubEnterpriseService,
33+
private val gitHubAPIService: GitHubAPIService,
3034
) {
3135

3236
private val logger = LoggerFactory.getLogger(GitHubScanningService::class.java)
@@ -36,17 +40,36 @@ class GitHubScanningService(
3640
val installations = getInstallations(jwtToken.toString())
3741
fetchAndSendOrganisationsData(installations)
3842
installations.forEach { installation ->
39-
fetchAndSendRepositoriesData(installation)
40-
.forEach { repository ->
41-
fetchManifestFilesAndSend(installation, repository)
43+
kotlin.runCatching {
44+
gitHubEnterpriseService.validateEnabledPermissionsAndEvents(
45+
INSTALLATION_LABEL,
46+
installation.permissions,
47+
installation.events
48+
)
49+
fetchAndSendRepositoriesData(installation)
50+
.forEach { repository ->
51+
fetchManifestFilesAndSend(installation, repository)
52+
}
53+
syncLogService.sendInfoLog("Finished initial full scan for organization ${installation.account.login}.")
54+
}.onFailure {
55+
val message = "Failed to scan organization ${installation.account.login}."
56+
when (it) {
57+
is GitHubAppInsufficientPermissionsException -> {
58+
syncLogService.sendErrorLog("$message ${it.message}")
59+
logger.error("$message ${it.message}")
60+
}
61+
else -> {
62+
syncLogService.sendErrorLog(message)
63+
logger.error(message, it)
64+
}
4265
}
43-
syncLogService.sendInfoLog("Finished initial full scan for organization ${installation.account.login}.")
66+
}
4467
}
4568
syncLogService.sendInfoLog("Finished full scan for all available organizations.")
4669
}
4770

4871
private fun getInstallations(jwtToken: String): List<Installation> {
49-
val installations = gitHubClient.getInstallations("Bearer $jwtToken")
72+
val installations = gitHubAPIService.getPaginatedInstallations(jwtToken)
5073
gitHubAuthenticationService.generateAndCacheInstallationTokens(installations, jwtToken)
5174
return installations
5275
}
@@ -61,7 +84,7 @@ class GitHubScanningService(
6184
}
6285
val installationToken = cachingService.get("installationToken:${installations.first().id}")
6386
val organizations = rateLimitHandler.executeWithRateLimitHandler(RateLimitType.REST) {
64-
gitHubClient.getOrganizations("Bearer $installationToken")
87+
gitHubAPIService.getPaginatedOrganizations(installationToken.toString())
6588
.map { organization ->
6689
if (installations.find { it.account.login == organization.login } != null) {
6790
OrganizationDto(organization.id, organization.login, true)
@@ -105,6 +128,7 @@ class GitHubScanningService(
105128
}
106129

107130
fun fetchManifestFilesAndSend(installation: Installation, repository: RepositoryDto) {
131+
if (repository.archived) return
108132
val manifestFiles = fetchManifestFiles(installation, repository.name).getOrThrow().items
109133
val manifestFilesContents = fetchManifestContents(
110134
installation,

src/main/kotlin/net/leanix/githubagent/services/WebhookEventService.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package net.leanix.githubagent.services
22

33
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
44
import com.fasterxml.jackson.module.kotlin.readValue
5-
import net.leanix.githubagent.dto.Account
6-
import net.leanix.githubagent.dto.Installation
5+
import net.leanix.githubagent.client.GitHubClient
76
import net.leanix.githubagent.dto.InstallationEventPayload
87
import net.leanix.githubagent.dto.ManifestFileAction
98
import net.leanix.githubagent.dto.ManifestFileUpdateDto
109
import net.leanix.githubagent.dto.PushEventCommit
1110
import net.leanix.githubagent.dto.PushEventPayload
11+
import net.leanix.githubagent.exceptions.JwtTokenNotFound
12+
import net.leanix.githubagent.shared.INSTALLATION_LABEL
1213
import net.leanix.githubagent.shared.MANIFEST_FILE_NAME
1314
import net.leanix.githubagent.shared.fileNameMatchRegex
1415
import net.leanix.githubagent.shared.generateFullPath
@@ -24,7 +25,9 @@ class WebhookEventService(
2425
private val gitHubAuthenticationService: GitHubAuthenticationService,
2526
private val gitHubScanningService: GitHubScanningService,
2627
private val syncLogService: SyncLogService,
27-
@Value("\${webhookEventService.waitingTime}") private val waitingTime: Long
28+
@Value("\${webhookEventService.waitingTime}") private val waitingTime: Long,
29+
private val gitHubClient: GitHubClient,
30+
private val gitHubEnterpriseService: GitHubEnterpriseService
2831
) {
2932

3033
private val logger = LoggerFactory.getLogger(WebhookEventService::class.java)
@@ -77,9 +80,15 @@ class WebhookEventService(
7780
}
7881
syncLogService.sendFullScanStart(installationEventPayload.installation.account.login)
7982
kotlin.runCatching {
80-
val installation = Installation(
83+
val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound()
84+
val installation = gitHubClient.getInstallation(
8185
installationEventPayload.installation.id.toLong(),
82-
Account(installationEventPayload.installation.account.login)
86+
"Bearer $jwtToken"
87+
)
88+
gitHubEnterpriseService.validateEnabledPermissionsAndEvents(
89+
INSTALLATION_LABEL,
90+
installation.permissions,
91+
installation.events
8392
)
8493
gitHubAuthenticationService.refreshTokens()
8594
gitHubScanningService.fetchAndSendOrganisationsData(listOf(installation))

src/main/kotlin/net/leanix/githubagent/shared/Constants.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ val SUPPORTED_EVENT_TYPES = listOf(
1313
)
1414

1515
val fileNameMatchRegex = Regex("/?$MANIFEST_FILE_NAME\$")
16+
17+
const val GITHUB_APP_LABEL = "GitHub App"
18+
const val INSTALLATION_LABEL = "Installation"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package net.leanix.githubagent.services
2+
3+
import io.mockk.every
4+
import io.mockk.mockk
5+
import net.leanix.githubagent.client.GitHubClient
6+
import net.leanix.githubagent.dto.Account
7+
import net.leanix.githubagent.dto.Installation
8+
import net.leanix.githubagent.dto.Organization
9+
import org.junit.jupiter.api.Assertions.assertEquals
10+
import org.junit.jupiter.api.Test
11+
12+
class GitHubAPIServiceTest {
13+
14+
private val gitHubClient = mockk<GitHubClient>()
15+
private val gitHubAPIService = GitHubAPIService(gitHubClient)
16+
17+
private val permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read")
18+
private val events = listOf("label", "public", "repository", "push")
19+
20+
@Test
21+
fun `test getPaginatedInstallations with one page`() {
22+
val jwtToken = "test-jwt-token"
23+
val installationsPage1 = listOf(
24+
Installation(1, Account("test-account"), permissions, events),
25+
Installation(2, Account("test-account"), permissions, events)
26+
)
27+
28+
every { gitHubClient.getInstallations(any(), any(), any()) } returns installationsPage1
29+
30+
val installations = gitHubAPIService.getPaginatedInstallations(jwtToken)
31+
assertEquals(2, installations.size)
32+
assertEquals(installationsPage1, installations)
33+
}
34+
35+
@Test
36+
fun `test getPaginatedInstallations with multiple pages`() {
37+
val jwtToken = "test-jwt-token"
38+
val perPage = 30
39+
val totalInstallations = 100
40+
val installations = (1..totalInstallations).map {
41+
Installation(it.toLong(), Account("test-account-$it"), permissions, events)
42+
}
43+
val pages = installations.chunked(perPage)
44+
45+
every { gitHubClient.getInstallations(any(), any(), any()) } returnsMany pages + listOf(emptyList())
46+
47+
val result = gitHubAPIService.getPaginatedInstallations(jwtToken)
48+
assertEquals(totalInstallations, result.size)
49+
assertEquals(installations, result)
50+
}
51+
52+
@Test
53+
fun `test getPaginatedOrganizations with one page`() {
54+
val installationToken = "test-installation-token"
55+
val organizationsPage1 = listOf(
56+
Organization("org-1", 1),
57+
Organization("org-2", 2)
58+
)
59+
60+
every { gitHubClient.getOrganizations(any(), any(), any()) } returns organizationsPage1
61+
62+
val organizations = gitHubAPIService.getPaginatedOrganizations(installationToken)
63+
assertEquals(2, organizations.size)
64+
assertEquals(organizationsPage1, organizations)
65+
}
66+
67+
@Test
68+
fun `test getPaginatedOrganizations with multiple pages`() {
69+
val installationToken = "test-installation-token"
70+
val perPage = 30
71+
val totalOrganizations = 100
72+
val organizations = (1..totalOrganizations).map { Organization("org-$it", it) }
73+
val pages = organizations.chunked(perPage)
74+
75+
every { gitHubClient.getOrganizations(any(), any(), any()) } returnsMany pages + listOf(emptyList())
76+
77+
val result = gitHubAPIService.getPaginatedOrganizations(installationToken)
78+
assertEquals(totalOrganizations, result.size)
79+
assertEquals(organizations, result)
80+
}
81+
}

0 commit comments

Comments
 (0)