From 1b041e546d6959d3bb80534c93c3d961c7d31d9f Mon Sep 17 00:00:00 2001 From: Jonas-Ian Kuche Date: Mon, 18 Nov 2024 21:37:27 +0100 Subject: [PATCH] feat(core): add course submission mode --- .../api/src/main/resources/application.yml | 3 ++ .../migrations/23_course_submission_mode.sql | 8 ++++++ .../ii/fbs/controller/CourseController.scala | 19 +++++++++---- .../fbs/controller/SubmissionController.scala | 14 +++++++++- .../scala/de/thm/ii/fbs/model/Course.scala | 2 +- .../services/persistence/CourseService.scala | 14 ++++++---- .../ii/fbs/services/security/IpService.scala | 28 +++++++++++++++++++ 7 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 modules/fbs-core/api/src/main/resources/migrations/23_course_submission_mode.sql create mode 100644 modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/security/IpService.scala diff --git a/modules/fbs-core/api/src/main/resources/application.yml b/modules/fbs-core/api/src/main/resources/application.yml index 4d3ac01b3..518e9ae0e 100644 --- a/modules/fbs-core/api/src/main/resources/application.yml +++ b/modules/fbs-core/api/src/main/resources/application.yml @@ -67,6 +67,9 @@ services: ids: salt: ${ID_SALT:feedbacksystem_id_salt} length: ${ID_LENGTH:8} + ip: + vpnCidrs: ${IP_VPN_CIDR:""} + localCidrs: ${IP_LOCAL_CIDR:""} spring: main: allow-bean-definition-overriding: true diff --git a/modules/fbs-core/api/src/main/resources/migrations/23_course_submission_mode.sql b/modules/fbs-core/api/src/main/resources/migrations/23_course_submission_mode.sql new file mode 100644 index 000000000..26d85b945 --- /dev/null +++ b/modules/fbs-core/api/src/main/resources/migrations/23_course_submission_mode.sql @@ -0,0 +1,8 @@ +BEGIN; + +ALTER TABLE course ADD COLUMN submission_mode ENUM('internet', 'vpn', 'local') NOT NULL DEFAULT 'internet'; + +INSERT INTO migration (number) VALUES (23); + +COMMIT; + diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala index 61d402b42..7cd877e57 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/CourseController.scala @@ -75,10 +75,13 @@ class CourseController { body.retrive("semesterId").asInt(), body.retrive("name").asText(), body.retrive("description").asText(), - body.retrive("visible").asBool() + body.retrive("visible").asBool(), + body.retrive("submissionMode").asText(), ) match { - case (semesterId, Some(name), desc, visible) => - courseService.create(Course(name, desc.getOrElse(""), visible.getOrElse(true), semesterId = semesterId)) + case (semesterId, Some(name), desc, visible, submissionMode) => + courseService.create( + Course(name, desc.getOrElse(""), visible.getOrElse(true), semesterId = semesterId, submissionMode = submissionMode.getOrElse("internet")) + ) case _ => throw new BadRequestException("Malformed Request Body") } } @@ -128,10 +131,14 @@ class CourseController { body.retrive("semesterId").asInt(), body.retrive("name").asText(), body.retrive("description").asText(), - body.retrive("visible").asBool() + body.retrive("visible").asBool(), + body.retrive("submissionMode").asText(), ) match { - case (semesterId, Some(name), desc, visible) => - courseService.update(cid, Course(name, desc.getOrElse(""), visible.getOrElse(true), semesterId = semesterId)) + case (semesterId, Some(name), desc, visible, submissionMode) => + courseService.update( + cid, + Course(name, desc.getOrElse(""), visible.getOrElse(true), semesterId = semesterId, submissionMode = submissionMode.getOrElse("internet")) + ) case _ => throw new BadRequestException("Malformed Request Body") } case _ => throw new ForbiddenException() diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/SubmissionController.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/SubmissionController.scala index 8aeeea624..8473e61e2 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/SubmissionController.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/controller/SubmissionController.scala @@ -7,7 +7,7 @@ import de.thm.ii.fbs.model.{CourseRole, GlobalRole, Submission} import de.thm.ii.fbs.services.checker.CheckerServiceFactoryService import de.thm.ii.fbs.services.persistence._ import de.thm.ii.fbs.services.persistence.storage.{MinioStorageService, StorageService} -import de.thm.ii.fbs.services.security.AuthService +import de.thm.ii.fbs.services.security.{AuthService, IpService} import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.io.InputStreamResource @@ -52,7 +52,11 @@ class SubmissionController { @Autowired private val courseService: CourseService = null @Autowired + private val ipService: IpService = null + @Autowired private val courseRegistration: CourseRegistrationService = null + @Autowired + private val request: HttpServletRequest = null private val objectMapper = new ObjectMapper(); private val logger = LoggerFactory.getLogger(this.getClass) @@ -137,6 +141,14 @@ class SubmissionController { def submit(@PathVariable("uid") uid: Int, @PathVariable("cid") cid: Int, @PathVariable("tid") tid: Int, @RequestParam file: MultipartFile, @RequestParam additionalInformation: Optional[String], req: HttpServletRequest, res: HttpServletResponse): Submission = { + val course = courseService.find(cid) + course match { + case Some(course) => + if (!ipService.isIpInZone(course.submissionMode, Option(request.getHeader("X-Real-IP")).getOrElse(request.getRemoteAddr))) { + throw new ForbiddenException() + } + case None => throw new ResourceNotFoundException() + } val user = authService.authorize(req, res) val someCourseRole = courseRegistration.getCourseRoleOfUser(cid, user.id) val noPrivateAccess = someCourseRole.contains(CourseRole.STUDENT) && user.globalRole != GlobalRole.ADMIN diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala index b2a7cf3f6..467e949e1 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/model/Course.scala @@ -10,4 +10,4 @@ package de.thm.ii.fbs.model * @param groupSelection Whether registration for groups is possible */ case class Course(name: String, description: String = "", visible: Boolean = true, id: Int = 0, semesterId: Option[Int] = None, - groupSelection: Option[Boolean] = None) + groupSelection: Option[Boolean] = None, submissionMode: String = "internet") diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala index e722e7704..eb96f5157 100644 --- a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/persistence/CourseService.scala @@ -35,7 +35,8 @@ class CourseService { * @return List of courses */ def findByPattern(pattern: String, ignoreHidden: Boolean = true): List[Course] = DB.query( - "SELECT course_id, group_selection, semester_id, name, description, visible FROM course WHERE name like ?" + (if (ignoreHidden) " AND visible = 1" else ""), + "SELECT course_id, group_selection, semester_id, name, description, visible, " + + "submission_mode FROM course WHERE name like ?" + (if (ignoreHidden) " AND visible = 1" else ""), (res, _) => parseResult(res), "%" + pattern + "%") /** @@ -45,7 +46,7 @@ class CourseService { * @return The found course */ def find(id: Int): Option[Course] = DB.query( - "SELECT course_id, group_selection, semester_id, name, description, visible, group_selection FROM course WHERE course_id = ?", + "SELECT course_id, group_selection, semester_id, name, description, visible, group_selection, submission_mode FROM course WHERE course_id = ?", (res, _) => parseResult(res), id).headOption /** @@ -55,8 +56,8 @@ class CourseService { * @return The created course with id */ def create(course: Course): Course = { - DB.insert("INSERT INTO course (semester_id, name, description, visible) VALUES (?,?,?,?);", - course.semesterId.orNull, course.name, course.description, course.visible) + DB.insert("INSERT INTO course (semester_id, name, description, visible, submission_mode) VALUES (?,?,?,?,?);", + course.semesterId.orNull, course.name, course.description, course.visible, course.submissionMode) .map(gk => gk(0).asInstanceOf[BigInteger].intValue()) .flatMap(id => find(id)) match { case Some(course) => course @@ -72,8 +73,8 @@ class CourseService { * @return True if successful */ def update(cid: Int, course: Course): Boolean = { - 1 == DB.update("UPDATE course SET semester_id = ?, name = ?, description = ?, visible = ? WHERE course_id = ?", - course.semesterId.orNull, course.name, course.description, course.visible, cid) + 1 == DB.update("UPDATE course SET semester_id = ?, name = ?, description = ?, visible = ?, submission_mode = ? WHERE course_id = ?", + course.semesterId.orNull, course.name, course.description, course.visible, course.submissionMode, cid) } /** @@ -90,6 +91,7 @@ class CourseService { name = res.getString("name"), description = res.getString("description"), visible = res.getBoolean("visible"), + submissionMode = res.getString("submission_mode"), id = res.getInt("course_id") ) diff --git a/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/security/IpService.scala b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/security/IpService.scala new file mode 100644 index 000000000..f666a7466 --- /dev/null +++ b/modules/fbs-core/api/src/main/scala/de/thm/ii/fbs/services/security/IpService.scala @@ -0,0 +1,28 @@ +package de.thm.ii.fbs.services.security + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.security.web.util.matcher.IpAddressMatcher + +@Component +class IpService { + @Value("${services.ip.vpnCidrs}") private var vpnIpRangeString: String = "" + @Value("${services.ip.localCidrs}") private var localIpRangeString: String = "" + + private def vpnIpRange: Seq[String] = vpnIpRangeString.split(",").filter(str => str.nonEmpty) + private def localIpRange: Seq[String] = localIpRangeString.split(",").filter(str => str.nonEmpty) + + private def isInCidr(cidr: String, ip: String): Boolean = { + new IpAddressMatcher(cidr).matches(ip) + } + + private def ipInCidrs(cirds: Seq[String], ip: String): Boolean = + cirds.exists(cidr => isInCidr(cidr, ip)) + + def isIpInZone(zone: String, ip: String): Boolean = + zone match { + case "internet" => true + case "vpn" => ipInCidrs(vpnIpRange, ip) || ipInCidrs(localIpRange, ip) + case "local" => ipInCidrs(localIpRange, ip) && !ipInCidrs(vpnIpRange, ip) + } +}