Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CID-1829: validate configuration #9

Merged
merged 12 commits into from
Aug 31, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.leanix.vsm.gitlab.broker.connector.adapter.feign

import jakarta.websocket.server.PathParam
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.gitlab.base-url}",
configuration = [GitlabFeignClientConfiguration::class]
)
interface GitlabClient {
@GetMapping("/user")
fun getCurrentUser(): GitlabUser

@GetMapping("/users/{userId}")
fun getUserById(@PathParam("userId") userId: Int): GitlabUser

@GetMapping("/projects/{projectName}")
fun getProjectByNameWithNamespace(@PathParam("projectName") projectName: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.leanix.vsm.gitlab.broker.connector.adapter.feign

import net.leanix.vsm.gitlab.broker.connector.adapter.feign.data.GitlabUser
import net.leanix.vsm.gitlab.broker.shared.exception.VsmException
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

@Component
class GitlabClientProvider(
private val gitlabClient: GitlabClient
) {

private val logger = LoggerFactory.getLogger(GitlabClientProvider::class.java)

fun getCurrentUser(): GitlabUser {
val user = kotlin.runCatching { gitlabClient.getCurrentUser() }
.onFailure {
logger.error("Invalid token, could not get current user")
throw VsmException.InvalidToken()
}.getOrThrow()
return runCatching { gitlabClient.getUserById(user.id) }
.onFailure { logger.error("Could not get user with id ${user.id}") }
.getOrThrow()
}

fun getOrg(orgName: String) {
runCatching { gitlabClient.getProjectByNameWithNamespace(orgName) }
.onFailure { logger.error("Could not get org info for $orgName") }
.getOrThrow()
}
}
Original file line number Diff line number Diff line change
@@ -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?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.leanix.vsm.gitlab.broker.connector.application

import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment
import net.leanix.vsm.gitlab.broker.logs.application.LoggingService
import net.leanix.vsm.gitlab.broker.logs.domain.AdminLog
import net.leanix.vsm.gitlab.broker.logs.domain.LogLevel
import net.leanix.vsm.gitlab.broker.logs.domain.LogStatus
import net.leanix.vsm.gitlab.broker.logs.domain.StatusLog
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.MessageSource
import java.util.*

open class BaseConnectorService {

@Autowired
lateinit var loggingService: LoggingService

@Autowired
lateinit var messageSource: MessageSource

private val logger = LoggerFactory.getLogger(BaseConnectorService::class.java)

fun logFailedStatus(message: String? = "empty message", runId: UUID) {
logger.error(message)
loggingService.sendStatusLog(
StatusLog(runId, LogStatus.FAILED, message)
)
}

fun logInfoMessages(code: String, arguments: Array<Any>, assignment: GitLabAssignment) {
val message = messageSource.getMessage(
code,
arguments,
Locale.ENGLISH
)
loggingService.sendAdminLog(
AdminLog(
runId = assignment.runId,
configurationId = assignment.configurationId,
subject = LogLevel.INFO.toString(),
level = LogLevel.INFO,
message = message
)
)
}

fun logFailedMessages(code: String, arguments: Array<Any>, assignment: GitLabAssignment) {
val message = messageSource.getMessage(
code,
arguments,
Locale.ENGLISH
)
loggingService.sendAdminLog(
AdminLog(
runId = assignment.runId,
configurationId = assignment.configurationId,
subject = LogLevel.ERROR.toString(),
level = LogLevel.ERROR,
message = message
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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.VsmException
import org.springframework.stereotype.Component
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

@Component
class ValidationService(
private val gitlabClientProvider: GitlabClientProvider
mohamedlajmileanix marked this conversation as resolved.
Show resolved Hide resolved
) : BaseConnectorService() {

fun validateConfiguration(gitLabAssignment: GitLabAssignment) {
val orgName = gitLabAssignment.connectorConfiguration.orgName
runCatching {
validateUserAccess()
validateOrgName(orgName)
}.onSuccess {
logInfoMessages("vsm.configuration.validation.successful", arrayOf(orgName), gitLabAssignment)
}.onFailure { exception ->
handleExceptions(exception, orgName, gitLabAssignment)
}
}

private fun validateUserAccess() {
run {
val user = gitlabClientProvider.getCurrentUser()
if (user.isAdmin != true) throw VsmException.AccessLevelValidationFailed()
}
}

private fun validateOrgName(orgName: String) {
runCatching {
gitlabClientProvider.getOrg(URLEncoder.encode(orgName, StandardCharsets.UTF_8.toString()))
mohamedlajmileanix marked this conversation as resolved.
Show resolved Hide resolved
}.onFailure {
throw VsmException.OrgNameValidationFailed()
}
}

private fun handleExceptions(
exception: Throwable,
orgName: String,
gitLabAssignment: GitLabAssignment
) {
when (exception) {
is VsmException.InvalidToken -> {
logFailedMessages("vsm.configuration.invalid_token", arrayOf(orgName), gitLabAssignment)
}

is VsmException.AccessLevelValidationFailed -> {
logFailedMessages("vsm.configuration.access_level", arrayOf(orgName), gitLabAssignment)
}

is VsmException.OrgNameValidationFailed -> {
logFailedMessages("vsm.configuration.invalid_org_name", arrayOf(orgName), gitLabAssignment)
}

is FeignException -> {
logFailedMessages("vsm.configuration.validation.failed", arrayOf(orgName), gitLabAssignment)
}
}
logFailedStatus(exception.message, gitLabAssignment.runId)
throw exception
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class GitlabWebhookFeignClientConfiguration(
class GitlabFeignClientConfiguration(

@Value("\${leanix.gitlab.access-token}") private val gitlabAccessToken: String
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.leanix.vsm.gitlab.broker.shared.exception

sealed class VsmException(message: String? = null) : RuntimeException(message) {

class InvalidToken : VsmException()

class AccessLevelValidationFailed : VsmException()

class OrgNameValidationFailed : VsmException()
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,7 +12,7 @@ import org.springframework.web.bind.annotation.RequestParam
@FeignClient(
name = "gitlabWebhookClient",
url = "\${leanix.gitlab.base-url}",
configuration = [GitlabWebhookFeignClientConfiguration::class]
configuration = [GitlabFeignClientConfiguration::class]
)
interface GitlabWebhookClient {

Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/messages.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
vsm.configuration.validation.successful=Validation successful for configuration with organisation name {0}
vsm.configuration.validation.failed=Validation failed for configuration with organisation name {0}
vsm.configuration.invalid_token=Invalid token in configuration with organisation name {0}
vsm.configuration.access_level=Access level insufficient for configuration with organisation name {0}
vsm.configuration.invalid_org_name=Invalid organisation name in configuration with organisation name {0}
mohamedlajmileanix marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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.GitlabClientProvider
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.VsmException
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<MessageSource>()
private val loggingService = mockk<LoggingService>()
private val gitlabClient = mockk<GitlabClient>()
private val gitlabClientProvider = GitlabClientProvider(gitlabClient)
private val validationService = ValidationService(gitlabClientProvider)

@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()
every { gitlabClient.getUserById(any()) } returns DataBuilder.getGitlabUser(true)
every { gitlabClient.getProjectByNameWithNamespace(any()) } returns Unit

validationService.validateConfiguration(DataBuilder.getGitlabAssignment())

verify(exactly = 1) { gitlabClient.getCurrentUser() }
verify(exactly = 1) { gitlabClient.getUserById(any()) }
verify(exactly = 1) { gitlabClient.getProjectByNameWithNamespace(any()) }
}

@Test
fun `it should not validate the configuration if token is invalid`() {
every { gitlabClient.getCurrentUser() } throws Exception()
every { gitlabClient.getUserById(any()) } returns DataBuilder.getGitlabUser(true)
every { gitlabClient.getProjectByNameWithNamespace(any()) } returns Unit

assertThrows<VsmException.InvalidToken> {
validationService.validateConfiguration(DataBuilder.getGitlabAssignment())
}

verify(exactly = 1) { gitlabClient.getCurrentUser() }
verify(exactly = 0) { gitlabClient.getUserById(any()) }
verify(exactly = 0) { gitlabClient.getProjectByNameWithNamespace(any()) }
}

@Test
fun `it should not validate the configuration if user is not admin`() {
every { gitlabClient.getCurrentUser() } returns DataBuilder.getGitlabCurrentUser()
every { gitlabClient.getUserById(any()) } returns DataBuilder.getGitlabUser(false)
every { gitlabClient.getProjectByNameWithNamespace(any()) } returns Unit

assertThrows<VsmException.AccessLevelValidationFailed> {
validationService.validateConfiguration(DataBuilder.getGitlabAssignment())
}

verify(exactly = 1) { gitlabClient.getCurrentUser() }
verify(exactly = 1) { gitlabClient.getUserById(any()) }
verify(exactly = 0) { gitlabClient.getProjectByNameWithNamespace(any()) }
}

@Test
fun `it should not validate the configuration if org name is invalid`() {
every { gitlabClient.getCurrentUser() } returns DataBuilder.getGitlabCurrentUser()
every { gitlabClient.getUserById(any()) } returns DataBuilder.getGitlabUser(true)
every { gitlabClient.getProjectByNameWithNamespace(any()) } throws Exception()

assertThrows<VsmException.OrgNameValidationFailed> {
validationService.validateConfiguration(DataBuilder.getGitlabAssignment())
}

verify(exactly = 1) { gitlabClient.getCurrentUser() }
verify(exactly = 1) { gitlabClient.getUserById(any()) }
verify(exactly = 1) { gitlabClient.getProjectByNameWithNamespace(any()) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.leanix.vsm.gitlab.broker.connector.shared

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("org-1")
)

fun getGitlabCurrentUser() = GitlabUser(
id = 1,
isAdmin = null
)

fun getGitlabUser(isAdmin: Boolean) = GitlabUser(
id = 1,
isAdmin = isAdmin
)
}
Loading