Skip to content

Commit 0bd5100

Browse files
Merge pull request #68 from leanix/feature/CID-3369/Validate-permissions-on-each-organisation-before-scanning
CID-3369: Validate permissions on each installation before scanning
2 parents 3c57f0a + 4132f31 commit 0bd5100

File tree

9 files changed

+123
-27
lines changed

9 files changed

+123
-27
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ interface GitHubClient {
3030
@GetMapping("/api/v3/app/installations")
3131
fun getInstallations(@RequestHeader("Authorization") jwt: String): List<Installation>
3232

33+
@GetMapping("/api/v3/app/installations/{installationId}")
34+
fun getInstallation(
35+
@PathVariable("installationId") installationId: Long,
36+
@RequestHeader("Authorization") jwt: String
37+
): Installation
38+
3339
@PostMapping("/api/v3/app/installations/{installationId}/access_tokens")
3440
fun createInstallationToken(
3541
@PathVariable("installationId") installationId: Long,

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)

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: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import net.leanix.githubagent.dto.ManifestFilesDTO
88
import net.leanix.githubagent.dto.Organization
99
import net.leanix.githubagent.dto.OrganizationDto
1010
import net.leanix.githubagent.dto.RepositoryDto
11+
import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException
1112
import net.leanix.githubagent.exceptions.JwtTokenNotFound
1213
import net.leanix.githubagent.exceptions.ManifestFileNotFoundException
1314
import net.leanix.githubagent.handler.RateLimitHandler
15+
import net.leanix.githubagent.shared.INSTALLATION_LABEL
1416
import net.leanix.githubagent.shared.MANIFEST_FILE_NAME
1517
import net.leanix.githubagent.shared.fileNameMatchRegex
1618
import net.leanix.githubagent.shared.generateFullPath
@@ -26,6 +28,7 @@ class GitHubScanningService(
2628
private val gitHubAuthenticationService: GitHubAuthenticationService,
2729
private val syncLogService: SyncLogService,
2830
private val rateLimitHandler: RateLimitHandler,
31+
private val gitHubEnterpriseService: GitHubEnterpriseService,
2932
) {
3033

3134
private val logger = LoggerFactory.getLogger(GitHubScanningService::class.java)
@@ -35,11 +38,30 @@ class GitHubScanningService(
3538
val installations = getInstallations(jwtToken.toString())
3639
fetchAndSendOrganisationsData(installations)
3740
installations.forEach { installation ->
38-
fetchAndSendRepositoriesData(installation)
39-
.forEach { repository ->
40-
fetchManifestFilesAndSend(installation, repository)
41+
kotlin.runCatching {
42+
gitHubEnterpriseService.validateEnabledPermissionsAndEvents(
43+
INSTALLATION_LABEL,
44+
installation.permissions,
45+
installation.events
46+
)
47+
fetchAndSendRepositoriesData(installation)
48+
.forEach { repository ->
49+
fetchManifestFilesAndSend(installation, repository)
50+
}
51+
syncLogService.sendInfoLog("Finished initial full scan for organization ${installation.account.login}.")
52+
}.onFailure {
53+
val message = "Failed to scan organization ${installation.account.login}."
54+
when (it) {
55+
is GitHubAppInsufficientPermissionsException -> {
56+
syncLogService.sendErrorLog("$message ${it.message}")
57+
logger.error("$message ${it.message}")
58+
}
59+
else -> {
60+
syncLogService.sendErrorLog(message)
61+
logger.error(message, it)
62+
}
4163
}
42-
syncLogService.sendInfoLog("Finished initial full scan for organization ${installation.account.login}.")
64+
}
4365
}
4466
syncLogService.sendInfoLog("Finished full scan for all available organizations.")
4567
}

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"

src/test/kotlin/net/leanix/githubagent/services/GitHubEnterpriseServiceTest.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@ import net.leanix.githubagent.client.GitHubClient
66
import net.leanix.githubagent.dto.GitHubAppResponse
77
import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException
88
import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException
9+
import net.leanix.githubagent.shared.GITHUB_APP_LABEL
910
import org.junit.jupiter.api.Assertions.assertThrows
11+
import org.junit.jupiter.api.BeforeEach
1012
import org.junit.jupiter.api.Test
1113
import org.junit.jupiter.api.assertDoesNotThrow
1214

1315
class GitHubEnterpriseServiceTest {
1416

1517
private val githubClient = mockk<GitHubClient>()
16-
private val service = GitHubEnterpriseService(githubClient)
18+
private val syncLogService = mockk<SyncLogService>()
19+
private val service = GitHubEnterpriseService(githubClient, syncLogService)
20+
21+
@BeforeEach
22+
fun setUp() {
23+
every { syncLogService.sendErrorLog(any()) } returns Unit
24+
}
1725

1826
@Test
1927
fun `verifyJwt with valid jwt should not throw exception`() {
@@ -44,7 +52,9 @@ class GitHubEnterpriseServiceTest {
4452
events = listOf("label", "public", "repository", "push", "installation")
4553
)
4654

47-
assertDoesNotThrow { service.validateGithubAppResponse(response) }
55+
assertDoesNotThrow {
56+
service.validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, response.permissions, response.events)
57+
}
4858
}
4959

5060
@Test
@@ -57,7 +67,7 @@ class GitHubEnterpriseServiceTest {
5767

5868
assertThrows(
5969
GitHubAppInsufficientPermissionsException::class.java
60-
) { service.validateGithubAppResponse(response) }
70+
) { service.validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, response.permissions, response.events) }
6171
}
6272

6373
@Test
@@ -70,6 +80,6 @@ class GitHubEnterpriseServiceTest {
7080

7181
assertThrows(
7282
GitHubAppInsufficientPermissionsException::class.java
73-
) { service.validateGithubAppResponse(response) }
83+
) { service.validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, response.permissions, response.events) }
7484
}
7585
}

src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.mockk.slot
66
import io.mockk.verify
77
import net.leanix.githubagent.client.GitHubClient
88
import net.leanix.githubagent.dto.Account
9+
import net.leanix.githubagent.dto.GitHubAppResponse
910
import net.leanix.githubagent.dto.GitHubSearchResponse
1011
import net.leanix.githubagent.dto.Installation
1112
import net.leanix.githubagent.dto.InstallationTokenResponse
@@ -34,22 +35,27 @@ class GitHubScanningServiceTest {
3435
private val gitHubAuthenticationService = mockk<GitHubAuthenticationService>()
3536
private val syncLogService = mockk<SyncLogService>(relaxUnitFun = true)
3637
private val rateLimitHandler = mockk<RateLimitHandler>(relaxUnitFun = true)
38+
private val gitHubEnterpriseService = GitHubEnterpriseService(gitHubClient, syncLogService)
3739
private val gitHubScanningService = GitHubScanningService(
3840
gitHubClient,
3941
cachingService,
4042
webSocketService,
4143
gitHubGraphQLService,
4244
gitHubAuthenticationService,
4345
syncLogService,
44-
rateLimitHandler
46+
rateLimitHandler,
47+
gitHubEnterpriseService,
4548
)
4649
private val runId = UUID.randomUUID()
4750

51+
private val permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read")
52+
private val events = listOf("label", "public", "repository", "push")
53+
4854
@BeforeEach
4955
fun setup() {
5056
every { cachingService.get(any()) } returns "value"
5157
every { gitHubClient.getInstallations(any()) } returns listOf(
52-
Installation(1, Account("testInstallation"))
58+
Installation(1, Account("testInstallation"), permissions, events)
5359
)
5460
every { gitHubClient.createInstallationToken(1, any()) } returns
5561
InstallationTokenResponse("testToken", "2024-01-01T00:00:00Z", mapOf(), "all")
@@ -66,6 +72,7 @@ class GitHubScanningServiceTest {
6672
every { syncLogService.sendInfoLog(any()) } returns Unit
6773
every { rateLimitHandler.executeWithRateLimitHandler(any<() -> Any>()) } answers
6874
{ firstArg<() -> Any>().invoke() }
75+
every { gitHubClient.getApp(any()) } returns GitHubAppResponse("testApp", permissions, events)
6976
}
7077

7178
@Test
@@ -225,4 +232,25 @@ class GitHubScanningServiceTest {
225232
verify { webSocketService.sendMessage(eq("$runId/manifestFiles"), capture(fileSlot)) }
226233
assertEquals(fileSlot.captured.manifestFiles[0].path, "")
227234
}
235+
236+
@Test
237+
fun `scanGitHubResources should skip organizations without correct permissions and events`() {
238+
every { cachingService.get("runId") } returns runId
239+
every { gitHubClient.getInstallations(any()) } returns listOf(
240+
Installation(1, Account("testInstallation1"), mapOf(), listOf()),
241+
Installation(2, Account("testInstallation2"), permissions, events),
242+
Installation(3, Account("testInstallation3"), permissions, events)
243+
)
244+
gitHubScanningService.scanGitHubResources()
245+
verify { webSocketService.sendMessage(eq("$runId/organizations"), any()) }
246+
verify {
247+
syncLogService.sendErrorLog(
248+
"Failed to scan organization testInstallation1. Installation missing " +
249+
"the following permissions: [administration, contents, metadata], " +
250+
"and the following events: [label, public, repository, push]"
251+
)
252+
}
253+
verify { syncLogService.sendInfoLog("Finished initial full scan for organization testInstallation2.") }
254+
verify { syncLogService.sendInfoLog("Finished initial full scan for organization testInstallation2.") }
255+
}
228256
}

src/test/kotlin/net/leanix/githubagent/services/WebhookEventServiceTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import io.mockk.just
66
import io.mockk.runs
77
import io.mockk.verify
88
import net.leanix.githubagent.client.GitHubClient
9+
import net.leanix.githubagent.dto.Account
10+
import net.leanix.githubagent.dto.Installation
911
import net.leanix.githubagent.dto.ManifestFileAction
1012
import net.leanix.githubagent.dto.ManifestFileUpdateDto
1113
import net.leanix.githubagent.dto.Organization
@@ -41,12 +43,18 @@ class WebhookEventServiceTest {
4143
@Autowired
4244
private lateinit var webhookEventService: WebhookEventService
4345

46+
private val permissions = mapOf("administration" to "read", "contents" to "read", "metadata" to "read")
47+
private val events = listOf("label", "public", "repository", "push")
48+
4449
@BeforeEach
4550
fun setUp() {
51+
val installation = Installation(1, Account("testInstallation"), permissions, events)
4652
every { gitHubAuthenticationService.refreshTokens() } returns Unit
4753
every { webSocketService.sendMessage(any(), any()) } returns Unit
4854
every { cachingService.get(any()) } returns "token"
4955
every { gitHubGraphQLService.getManifestFileContent(any(), any(), any(), any()) } returns "content"
56+
every { gitHubClient.getInstallations(any()) } returns listOf(installation)
57+
every { gitHubClient.getInstallation(any(), any()) } returns installation
5058
}
5159

5260
@Test

0 commit comments

Comments
 (0)