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..fc31afa --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClient.kt @@ -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) +} 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..7994b51 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/GitlabClientProvider.kt @@ -0,0 +1,29 @@ +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 + +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() + } +} 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 new file mode 100644 index 0000000..97fe809 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/BaseConnectorService.kt @@ -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 + private lateinit var loggingService: LoggingService + + @Autowired + private 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, 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, 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 + ) + ) + } +} 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..261be0a --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/application/ValidationService.kt @@ -0,0 +1,67 @@ +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 + +@Component +class ValidationService( + private val gitlabClientProvider: GitlabClientProvider +) : BaseConnectorService() { + + fun validateConfiguration(gitLabAssignment: GitLabAssignment) { + val orgName = gitLabAssignment.connectorConfiguration.orgName + runCatching { + validateUserAccess() + validateOrgName(gitLabAssignment.connectorConfiguration.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, "UTF-8")) + }.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 + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt index 6543dde..9baa4d3 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt @@ -1,6 +1,7 @@ package net.leanix.vsm.gitlab.broker.connector.runner import net.leanix.vsm.gitlab.broker.connector.application.AssignmentService +import net.leanix.vsm.gitlab.broker.connector.application.ValidationService import net.leanix.vsm.gitlab.broker.shared.cache.AssignmentsCache import net.leanix.vsm.gitlab.broker.webhook.domain.WebhookService import org.slf4j.Logger @@ -12,6 +13,7 @@ import org.springframework.stereotype.Component @Component class InitialStateRunner( private val assignmentService: AssignmentService, + private val validationService: ValidationService, private val webhookService: WebhookService ) : ApplicationRunner { @@ -30,6 +32,7 @@ class InitialStateRunner( "Received assignment for ${assignment.connectorConfiguration.orgName} " + "with configuration id: ${assignment.configurationId} and with run id: ${assignment.runId}" ) + validationService.validateConfiguration(assignment) } }.onSuccess { logger.info("Cached ${AssignmentsCache.getAll().size} assignments") 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 723c655..2e6aecf 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.gitlab.access-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 new file mode 100644 index 0000000..998bd1e --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/exception/VsmException.kt @@ -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() +} 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 315a066..b57fa27 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.gitlab.base-url}", - configuration = [GitlabWebhookFeignClientConfiguration::class] + configuration = [GitlabFeignClientConfiguration::class] ) interface GitlabWebhookClient { diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..416e245 --- /dev/null +++ b/src/main/resources/messages.properties @@ -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}