Skip to content

Commit

Permalink
CID-3369: Validate permissions on each organisation before scanning
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamedlajmileanix committed Jan 2, 2025
1 parent d2b3bd4 commit 4526b50
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 27 deletions.
6 changes: 6 additions & 0 deletions src/main/kotlin/net/leanix/githubagent/client/GitHubClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ interface GitHubClient {
@GetMapping("/api/v3/app/installations")
fun getInstallations(@RequestHeader("Authorization") jwt: String): List<Installation>

@GetMapping("/api/v3/app/installations/{installationId}")
fun getInstallation(
@PathVariable("installationId") installationId: Long,
@RequestHeader("Authorization") jwt: String
): Installation

@PostMapping("/api/v3/app/installations/{installationId}/access_tokens")
fun createInstallationToken(
@PathVariable("installationId") installationId: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ data class InstallationTokenResponse(
@JsonIgnoreProperties(ignoreUnknown = true)
data class Installation(
@JsonProperty("id") val id: Long,
@JsonProperty("account") val account: Account
@JsonProperty("account") val account: Account,
@JsonProperty("permissions") val permissions: Map<String, String>,
@JsonProperty("events") val events: List<String>
)

@JsonIgnoreProperties(ignoreUnknown = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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.shared.GITHUB_APP_LABEL
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class GitHubEnterpriseService(private val githubClient: GitHubClient) {
class GitHubEnterpriseService(
private val githubClient: GitHubClient,
private val syncLogService: SyncLogService,
) {

companion object {
val expectedPermissions = listOf("administration", "contents", "metadata")
Expand All @@ -19,29 +22,34 @@ class GitHubEnterpriseService(private val githubClient: GitHubClient) {
fun verifyJwt(jwt: String) {
runCatching {
val githubApp = getGitHubApp(jwt)
validateGithubAppResponse(githubApp)
validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, githubApp.permissions, githubApp.events)
logger.info("Authenticated as GitHub App: '${githubApp.slug}'")
}.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 -> {
syncLogService.sendErrorLog(it.message!!)
logger.error(it.message)
}
else -> {
logger.error("Failed to verify JWT token", it)
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) }
fun validateEnabledPermissionsAndEvents(type: String, permissions: Map<String, String>, events: List<String>) {
val missingPermissions = expectedPermissions.filterNot { permissions.containsKey(it) }
val missingEvents = expectedEvents.filterNot { events.contains(it) }

if (missingPermissions.isNotEmpty() || missingEvents.isNotEmpty()) {
var message = "GitHub App is missing the following "
var message = "$type 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(", and the following ")
}
message = message.plus("events: $missingEvents")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import net.leanix.githubagent.dto.ManifestFilesDTO
import net.leanix.githubagent.dto.Organization
import net.leanix.githubagent.dto.OrganizationDto
import net.leanix.githubagent.dto.RepositoryDto
import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException
import net.leanix.githubagent.exceptions.JwtTokenNotFound
import net.leanix.githubagent.exceptions.ManifestFileNotFoundException
import net.leanix.githubagent.handler.RateLimitHandler
import net.leanix.githubagent.shared.INSTALLATION_LABEL
import net.leanix.githubagent.shared.MANIFEST_FILE_NAME
import net.leanix.githubagent.shared.fileNameMatchRegex
import net.leanix.githubagent.shared.generateFullPath
Expand All @@ -26,6 +28,7 @@ class GitHubScanningService(
private val gitHubAuthenticationService: GitHubAuthenticationService,
private val syncLogService: SyncLogService,
private val rateLimitHandler: RateLimitHandler,
private val gitHubEnterpriseService: GitHubEnterpriseService,
) {

private val logger = LoggerFactory.getLogger(GitHubScanningService::class.java)
Expand All @@ -35,11 +38,30 @@ class GitHubScanningService(
val installations = getInstallations(jwtToken.toString())
fetchAndSendOrganisationsData(installations)
installations.forEach { installation ->
fetchAndSendRepositoriesData(installation)
.forEach { repository ->
fetchManifestFilesAndSend(installation, repository)
kotlin.runCatching {
gitHubEnterpriseService.validateEnabledPermissionsAndEvents(
INSTALLATION_LABEL,
installation.permissions,
installation.events
)
fetchAndSendRepositoriesData(installation)
.forEach { repository ->
fetchManifestFilesAndSend(installation, repository)
}
syncLogService.sendInfoLog("Finished initial full scan for organization ${installation.account.login}.")
}.onFailure {
val message = "Failed to scan organization ${installation.account.login}."
when (it) {
is GitHubAppInsufficientPermissionsException -> {
syncLogService.sendErrorLog("$message ${it.message}")
logger.error("$message ${it.message}")
}
else -> {
syncLogService.sendErrorLog(message)
logger.error(message, it)
}
}
syncLogService.sendInfoLog("Finished initial full scan for organization ${installation.account.login}.")
}
}
syncLogService.sendInfoLog("Finished full scan for all available organizations.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package net.leanix.githubagent.services

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import net.leanix.githubagent.dto.Account
import net.leanix.githubagent.dto.Installation
import net.leanix.githubagent.client.GitHubClient
import net.leanix.githubagent.dto.InstallationEventPayload
import net.leanix.githubagent.dto.ManifestFileAction
import net.leanix.githubagent.dto.ManifestFileUpdateDto
import net.leanix.githubagent.dto.PushEventCommit
import net.leanix.githubagent.dto.PushEventPayload
import net.leanix.githubagent.exceptions.JwtTokenNotFound
import net.leanix.githubagent.shared.INSTALLATION_LABEL
import net.leanix.githubagent.shared.MANIFEST_FILE_NAME
import net.leanix.githubagent.shared.fileNameMatchRegex
import net.leanix.githubagent.shared.generateFullPath
Expand All @@ -24,7 +25,9 @@ class WebhookEventService(
private val gitHubAuthenticationService: GitHubAuthenticationService,
private val gitHubScanningService: GitHubScanningService,
private val syncLogService: SyncLogService,
@Value("\${webhookEventService.waitingTime}") private val waitingTime: Long
@Value("\${webhookEventService.waitingTime}") private val waitingTime: Long,
private val gitHubClient: GitHubClient,
private val gitHubEnterpriseService: GitHubEnterpriseService
) {

private val logger = LoggerFactory.getLogger(WebhookEventService::class.java)
Expand Down Expand Up @@ -77,9 +80,15 @@ class WebhookEventService(
}
syncLogService.sendFullScanStart(installationEventPayload.installation.account.login)
kotlin.runCatching {
val installation = Installation(
val jwtToken = cachingService.get("jwtToken") ?: throw JwtTokenNotFound()
val installation = gitHubClient.getInstallation(
installationEventPayload.installation.id.toLong(),
Account(installationEventPayload.installation.account.login)
"Bearer $jwtToken"
)
gitHubEnterpriseService.validateEnabledPermissionsAndEvents(
INSTALLATION_LABEL,
installation.permissions,
installation.events
)
gitHubAuthenticationService.refreshTokens()
gitHubScanningService.fetchAndSendRepositoriesData(installation).forEach { repository ->
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/net/leanix/githubagent/shared/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ val SUPPORTED_EVENT_TYPES = listOf(
)

val fileNameMatchRegex = Regex("/?$MANIFEST_FILE_NAME\$")

const val GITHUB_APP_LABEL = "GitHub App"
const val INSTALLATION_LABEL = "Installation"
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ 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.shared.GITHUB_APP_LABEL
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow

class GitHubEnterpriseServiceTest {

private val githubClient = mockk<GitHubClient>()
private val service = GitHubEnterpriseService(githubClient)
private val syncLogService = mockk<SyncLogService>()
private val service = GitHubEnterpriseService(githubClient, syncLogService)

@BeforeEach
fun setUp() {
every { syncLogService.sendErrorLog(any()) } returns Unit
}

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

assertDoesNotThrow { service.validateGithubAppResponse(response) }
assertDoesNotThrow {
service.validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, response.permissions, response.events)
}
}

@Test
Expand All @@ -57,7 +67,7 @@ class GitHubEnterpriseServiceTest {

assertThrows(
GitHubAppInsufficientPermissionsException::class.java
) { service.validateGithubAppResponse(response) }
) { service.validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, response.permissions, response.events) }
}

@Test
Expand All @@ -70,6 +80,6 @@ class GitHubEnterpriseServiceTest {

assertThrows(
GitHubAppInsufficientPermissionsException::class.java
) { service.validateGithubAppResponse(response) }
) { service.validateEnabledPermissionsAndEvents(GITHUB_APP_LABEL, response.permissions, response.events) }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package net.leanix.githubagent.services

import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.slot
import io.mockk.verify
import net.leanix.githubagent.client.GitHubClient
Expand Down Expand Up @@ -34,22 +36,24 @@ class GitHubScanningServiceTest {
private val gitHubAuthenticationService = mockk<GitHubAuthenticationService>()
private val syncLogService = mockk<SyncLogService>(relaxUnitFun = true)
private val rateLimitHandler = mockk<RateLimitHandler>(relaxUnitFun = true)
private val gitHubEnterpriseService = mockk<GitHubEnterpriseService>(relaxUnitFun = true)
private val gitHubScanningService = GitHubScanningService(
gitHubClient,
cachingService,
webSocketService,
gitHubGraphQLService,
gitHubAuthenticationService,
syncLogService,
rateLimitHandler
rateLimitHandler,
gitHubEnterpriseService,
)
private val runId = UUID.randomUUID()

@BeforeEach
fun setup() {
every { cachingService.get(any()) } returns "value"
every { gitHubClient.getInstallations(any()) } returns listOf(
Installation(1, Account("testInstallation"))
Installation(1, Account("testInstallation"), mapOf(), listOf())
)
every { gitHubClient.createInstallationToken(1, any()) } returns
InstallationTokenResponse("testToken", "2024-01-01T00:00:00Z", mapOf(), "all")
Expand All @@ -66,6 +70,7 @@ class GitHubScanningServiceTest {
every { syncLogService.sendInfoLog(any()) } returns Unit
every { rateLimitHandler.executeWithRateLimitHandler(any<() -> Any>()) } answers
{ firstArg<() -> Any>().invoke() }
every { gitHubEnterpriseService.validateEnabledPermissionsAndEvents(any(), any(), any()) } just runs
}

@Test
Expand Down

0 comments on commit 4526b50

Please sign in to comment.