diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClient.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClient.kt new file mode 100644 index 0000000..0a44f47 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClient.kt @@ -0,0 +1,20 @@ +package net.leanix.vsm.gitlab.broker.connector.adapter.feign + +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabGroup +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabUser +import net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.GitlabFeignClientConfiguration +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping + +@FeignClient( + name = "gitlab", + url = "\${leanix.vsm.connector.gitlab-url}/api/v4", + configuration = [GitlabFeignClientConfiguration::class] +) +interface GitlabClient { + @GetMapping("/user") + fun getCurrentUser(): GitlabUser + + @GetMapping("/groups") + fun getAllGroups(): List +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClientProvider.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClientProvider.kt new file mode 100644 index 0000000..11afecb --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClientProvider.kt @@ -0,0 +1,9 @@ +package net.leanix.vsm.gitlab.broker.connector.adapter.feign + +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabGroup +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabUser + +interface GitlabClientProvider { + fun getCurrentUser(): GitlabUser + fun getGroupByFullPath(fullPath: String): GitlabGroup? +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabFeignClientProvider.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabFeignClientProvider.kt new file mode 100644 index 0000000..8e152b5 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabFeignClientProvider.kt @@ -0,0 +1,29 @@ +package net.leanix.vsm.gitlab.broker.connector.adapter.feign + +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabGroup +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabUser +import net.leanix.vsm.gitlab.broker.shared.exception.InvalidToken +import net.leanix.vsm.gitlab.broker.shared.exception.OrgNameValidationFailed +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class GitlabFeignClientProvider( + private val gitlabClient: GitlabClient +) : GitlabClientProvider { + + private val logger = LoggerFactory.getLogger(GitlabFeignClientProvider::class.java) + + override fun getCurrentUser(): GitlabUser { + return runCatching { gitlabClient.getCurrentUser() }.onFailure { + logger.error("Invalid token, could not get current user") + throw InvalidToken() + }.getOrThrow() + } + + override fun getGroupByFullPath(fullPath: String): GitlabGroup? { + return runCatching { + gitlabClient.getAllGroups().first { it.fullPath == fullPath } + }.onFailure { throw OrgNameValidationFailed() }.getOrThrow() + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/data/GitlabGroup.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/data/GitlabGroup.kt new file mode 100644 index 0000000..a579712 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/data/GitlabGroup.kt @@ -0,0 +1,10 @@ +package net.leanix.vsm.gitlab.broker.connector.adapter.feign.data + +import com.fasterxml.jackson.annotation.JsonProperty + +class GitlabGroup( + @JsonProperty("id") + val id: Int, + @JsonProperty("full_path") + val fullPath: String +) diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/data/GitlabUser.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/data/GitlabUser.kt new file mode 100644 index 0000000..8aecae1 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/data/GitlabUser.kt @@ -0,0 +1,10 @@ +package net.leanix.vsm.gitlab.broker.connector.adapter.feign.data + +import com.fasterxml.jackson.annotation.JsonProperty + +class GitlabUser( + @JsonProperty("id") + val id: Int, + @JsonProperty("is_admin") + val isAdmin: Boolean? +) diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/BaseConnectorService.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/BaseConnectorService.kt index b88e8db..bc11887 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/BaseConnectorService.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/BaseConnectorService.kt @@ -14,10 +14,10 @@ import java.util.Locale open class BaseConnectorService { @Autowired - private lateinit var loggingService: LoggingService + lateinit var loggingService: LoggingService @Autowired - private lateinit var messageSource: MessageSource + lateinit var messageSource: MessageSource private val logger = KotlinLogging.logger {} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationService.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationService.kt new file mode 100644 index 0000000..a93da78 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationService.kt @@ -0,0 +1,61 @@ +package net.leanix.vsm.gitlab.broker.connector.application + +import feign.FeignException +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.GitlabClientProvider +import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment +import net.leanix.vsm.gitlab.broker.shared.exception.AccessLevelValidationFailed +import net.leanix.vsm.gitlab.broker.shared.exception.InvalidToken +import net.leanix.vsm.gitlab.broker.shared.exception.OrgNameValidationFailed +import org.springframework.stereotype.Component + +@Component +class ValidationService( + private val gitlabClientProvider: GitlabClientProvider +) : BaseConnectorService() { + + fun validateConfiguration(gitLabAssignment: GitLabAssignment) { + val orgName = gitLabAssignment.connectorConfiguration.orgName + runCatching { + validateUserAccess() + validateGroupPath(orgName) + }.onSuccess { + logInfoMessages("vsm.configuration.validation.successful", arrayOf(orgName), gitLabAssignment) + }.onFailure { exception -> + handleExceptions(exception, orgName, gitLabAssignment) + } + } + + private fun validateUserAccess() { + if (gitlabClientProvider.getCurrentUser().isAdmin != true) throw AccessLevelValidationFailed() + } + + private fun validateGroupPath(fullPath: String) { + if (gitlabClientProvider.getGroupByFullPath(fullPath) == null) throw OrgNameValidationFailed() + } + + private fun handleExceptions( + exception: Throwable, + orgName: String, + gitLabAssignment: GitLabAssignment + ) { + when (exception) { + is InvalidToken -> { + logFailedMessages("vsm.configuration.invalid_token", arrayOf(orgName), gitLabAssignment) + } + + is AccessLevelValidationFailed -> { + logFailedMessages("vsm.configuration.access_level", arrayOf(orgName), gitLabAssignment) + } + + is OrgNameValidationFailed -> { + logFailedMessages("vsm.configuration.invalid_org_name", arrayOf(orgName), gitLabAssignment) + } + + is FeignException -> { + logFailedMessages("vsm.configuration.validation.failed", arrayOf(orgName), gitLabAssignment) + } + } + logFailedStatus(exception.message, gitLabAssignment) + throw exception + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookFeignClientConfiguration.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/GitlabFeignClientConfiguration.kt similarity index 82% rename from src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookFeignClientConfiguration.kt rename to src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/GitlabFeignClientConfiguration.kt index 338380a..0a4aa67 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookFeignClientConfiguration.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/GitlabFeignClientConfiguration.kt @@ -1,4 +1,4 @@ -package net.leanix.vsm.gitlab.broker.webhook.adapter.feign +package net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign import feign.RequestInterceptor import org.springframework.beans.factory.annotation.Value @@ -6,7 +6,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class GitlabWebhookFeignClientConfiguration( +class GitlabFeignClientConfiguration( @Value("\${leanix.vsm.connector.gitlab-token}") private val gitlabAccessToken: String ) { diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/exception/VsmException.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/exception/VsmException.kt index 00f301b..a87c960 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/exception/VsmException.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/exception/VsmException.kt @@ -5,3 +5,9 @@ sealed class VsmException(message: String? = null) : RuntimeException(message) class NoRepositoriesFound : VsmException() class GraphqlException(message: String?) : VsmException(message) + +class InvalidToken : VsmException() + +class AccessLevelValidationFailed : VsmException() + +class OrgNameValidationFailed : VsmException() diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookClient.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookClient.kt index 4f3b180..b38a3be 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookClient.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/webhook/adapter/feign/GitlabWebhookClient.kt @@ -1,5 +1,6 @@ package net.leanix.vsm.gitlab.broker.webhook.adapter.feign +import net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.GitlabFeignClientConfiguration import net.leanix.vsm.gitlab.broker.webhook.domain.GitlabWebhook import org.springframework.cloud.openfeign.FeignClient import org.springframework.web.bind.annotation.DeleteMapping @@ -11,7 +12,7 @@ import org.springframework.web.bind.annotation.RequestParam @FeignClient( name = "gitlabWebhookClient", url = "\${leanix.vsm.connector.gitlab-url}/api/v4/hooks", - configuration = [GitlabWebhookFeignClientConfiguration::class] + configuration = [GitlabFeignClientConfiguration::class] ) interface GitlabWebhookClient { diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 3048955..9bb6f91 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,3 +1,8 @@ vsm.repos.not_found=Zero repositories found in {0} GitHub organisation. Hint: In case organisation is valid, check if the inclusion list has at least one valid repository name. vsm.repos.total=Fetched Org Repositories Ids. Result : {0} repos -vsm.repos.imported=Repository Imported \ No newline at end of file +vsm.repos.imported=Repository Imported +vsm.configuration.validation.successful=Validation successful for configuration with group/subgroup path {0} +vsm.configuration.validation.failed=Validation failed for configuration with group/subgroup path {0} +vsm.configuration.invalid_token=Invalid token in configuration with group/subgroup path {0} +vsm.configuration.access_level=Access level insufficient for configuration with group/subgroup path {0} +vsm.configuration.invalid_org_name=Invalid group/subgroup path in configuration with group/subgroup path {0} diff --git a/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationServiceTest.kt b/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationServiceTest.kt new file mode 100644 index 0000000..7b2f4ff --- /dev/null +++ b/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationServiceTest.kt @@ -0,0 +1,85 @@ +package net.leanix.vsm.gitlab.broker.connector.application + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.GitlabClient +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.GitlabFeignClientProvider +import net.leanix.vsm.gitlab.broker.connector.shared.DataBuilder +import net.leanix.vsm.gitlab.broker.logs.application.LoggingService +import net.leanix.vsm.gitlab.broker.shared.exception.AccessLevelValidationFailed +import net.leanix.vsm.gitlab.broker.shared.exception.InvalidToken +import net.leanix.vsm.gitlab.broker.shared.exception.OrgNameValidationFailed +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.context.MessageSource + +class ValidationServiceTest { + + private val messageSource = mockk() + private val loggingService = mockk() + private val gitlabClient = mockk() + private val gitlabFeignClientProvider = GitlabFeignClientProvider(gitlabClient) + private val validationService = ValidationService(gitlabFeignClientProvider) + + @BeforeEach + fun setUp() { + validationService.messageSource = messageSource + validationService.loggingService = loggingService + + every { messageSource.getMessage(allAny(), allAny(), allAny()) } returns "mock-message" + every { loggingService.sendAdminLog(any()) } returns Unit + every { loggingService.sendStatusLog(any()) } returns Unit + } + + @Test + fun `it should validate the configuration`() { + every { gitlabClient.getCurrentUser() } returns DataBuilder.getGitlabCurrentUser(true) + every { gitlabClient.getAllGroups() } returns DataBuilder.getAllGroups() + + validationService.validateConfiguration(DataBuilder.getGitlabAssignment()) + + verify(exactly = 1) { gitlabClient.getCurrentUser() } + verify(exactly = 1) { gitlabClient.getAllGroups() } + } + + @Test + fun `it should not validate the configuration if token is invalid`() { + every { gitlabClient.getCurrentUser() } throws Exception() + every { gitlabClient.getAllGroups() } returns DataBuilder.getAllGroups() + + assertThrows { + validationService.validateConfiguration(DataBuilder.getGitlabAssignment()) + } + + verify(exactly = 1) { gitlabClient.getCurrentUser() } + verify(exactly = 0) { gitlabClient.getAllGroups() } + } + + @Test + fun `it should not validate the configuration if user is not admin`() { + every { gitlabClient.getCurrentUser() } returns DataBuilder.getGitlabCurrentUser(false) + every { gitlabClient.getAllGroups() } returns DataBuilder.getAllGroups() + + assertThrows { + validationService.validateConfiguration(DataBuilder.getGitlabAssignment()) + } + + verify(exactly = 1) { gitlabClient.getCurrentUser() } + verify(exactly = 0) { gitlabClient.getAllGroups() } + } + + @Test + fun `it should not validate the configuration if group name is invalid`() { + every { gitlabClient.getCurrentUser() } returns DataBuilder.getGitlabCurrentUser(true) + every { gitlabClient.getAllGroups() } throws Exception() + + assertThrows { + validationService.validateConfiguration(DataBuilder.getGitlabAssignment()) + } + + verify(exactly = 1) { gitlabClient.getCurrentUser() } + verify(exactly = 1) { gitlabClient.getAllGroups() } + } +} diff --git a/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/shared/DataBuilder.kt b/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/shared/DataBuilder.kt new file mode 100644 index 0000000..1e5eb86 --- /dev/null +++ b/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/shared/DataBuilder.kt @@ -0,0 +1,33 @@ +package net.leanix.vsm.gitlab.broker.connector.shared + +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabGroup +import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabUser +import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment +import net.leanix.vsm.gitlab.broker.connector.domain.GitLabConfiguration +import java.util.* + +object DataBuilder { + + fun getGitlabAssignment() = GitLabAssignment( + runId = UUID.randomUUID(), + workspaceId = UUID.randomUUID(), + configurationId = UUID.randomUUID(), + connectorConfiguration = GitLabConfiguration("group-1") + ) + + fun getGitlabCurrentUser(isAdmin: Boolean) = GitlabUser( + id = 1, + isAdmin = isAdmin + ) + + fun getAllGroups() = listOf( + GitlabGroup( + id = 1, + fullPath = "group-1", + ), + GitlabGroup( + id = 2, + fullPath = "group-2", + ) + ) +}