From 264fcf77b06870dee2623928400e4c27d358ff52 Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Tue, 19 Dec 2023 18:20:14 +0100 Subject: [PATCH] Support GitHub-style (HmacSHA256 signature) git hooks --- .../ifi/access/controller/CourseController.kt | 16 ++++++++-- .../uzh/ifi/access/service/CourseService.kt | 30 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt b/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt index d224ca7..d77ce69 100644 --- a/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt +++ b/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt @@ -45,10 +45,20 @@ class WebhooksController( private val logger = KotlinLogging.logger {} @PostMapping("/courses/{course}/update/gitlab") - fun updateCourse(@PathVariable("course") course: String, + fun hookGitlab(@PathVariable("course") course: String, @RequestHeader("X-Gitlab-Token") secret: String) { - logger.debug { "webhook triggered for $course"} - courseService.webhookUpdateCourse(course, secret) + logger.debug { "webhook (secret) triggered for $course"} + courseService.webhookUpdateWithSecret(course, secret) + } + @PostMapping("/courses/{course}/update/github") + fun hookGithub(@PathVariable("course") course: String, + @RequestHeader("X-Hub-Signature-256") signature: String, + @RequestBody body: String + ) { + logger.debug { "webhook (signature) triggered for $course"} + val sig = signature.substringAfter("sha256=") + logger.debug { "PAYLOAD: ((($body))) SIG: ((($sig)))"} + courseService.webhookUpdateWithSignature(course, sig, body) } } diff --git a/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt b/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt index b28c7e1..13a629e 100644 --- a/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt +++ b/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt @@ -16,6 +16,7 @@ import com.github.dockerjava.api.model.Bind import com.github.dockerjava.api.model.HostConfig import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.transaction.Transactional +import jakarta.xml.bind.DatatypeConverter import org.apache.commons.collections4.ListUtils import org.apache.commons.io.FileUtils import org.apache.logging.log4j.util.Strings @@ -35,6 +36,7 @@ import java.nio.charset.Charset import java.nio.file.Files import java.nio.file.NoSuchFileException import java.nio.file.Path +import java.security.MessageDigest import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* @@ -43,6 +45,9 @@ import java.util.concurrent.TimeUnit import java.util.function.Consumer import java.util.stream.Collectors import java.util.stream.Stream +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.math.sign @Service class CourseServiceForCaching( @@ -554,10 +559,29 @@ fi } @Transactional - fun webhookUpdateCourse(courseSlug: String, secret: String): Course? { + fun webhookUpdateWithSecret(courseSlug: String, secret: String?): Course? { val existingCourse = getCourseBySlug(courseSlug) - if (existingCourse.webhookSecret != null && existingCourse.webhookSecret == secret) { - return updateCourse(courseSlug) + if (existingCourse.webhookSecret != null && secret != null) { + if (existingCourse.webhookSecret == secret) { + return updateCourse(courseSlug) + } + } + logger.debug { "Provided webhook secret does not match secret of course $courseSlug"} + throw ResponseStatusException(HttpStatus.FORBIDDEN) + } + + fun webhookUpdateWithSignature(courseSlug: String, signature: String?, body: String): Course? { + val existingCourse = getCourseBySlug(courseSlug) + if (existingCourse.webhookSecret != null && signature != null) { + val signingKey = SecretKeySpec(existingCourse.webhookSecret!!.toByteArray(Charsets.UTF_8), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(signingKey) + val hash = mac.doFinal(body.toByteArray(Charsets.UTF_8)) + val expected = DatatypeConverter.printHexBinary(hash) + logger.info{"Expected: ${expected} based on secret ${existingCourse.webhookSecret}, actual: ${signature}"} + if (expected.equals(signature, ignoreCase = true)) { + return updateCourse(courseSlug) + } } logger.debug { "Provided webhook secret does not match secret of course $courseSlug"} throw ResponseStatusException(HttpStatus.FORBIDDEN)