diff --git a/.idea/kubernetes-settings.xml b/.idea/kubernetes-settings.xml index 0207c4b0843da6a8315f3419ea96141cdcdebe10..1ba17a0c33b44b03beea3ae22c5b829a2f7e32f6 100644 --- a/.idea/kubernetes-settings.xml +++ b/.idea/kubernetes-settings.xml @@ -9,4 +9,7 @@ </list> </option> </component> + <component name="KubernetesSettings"> + <option name="contextName" value="minikube" /> + </component> </project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 2acfb33066ba1138f2ea4b1e9f0e2ecb3ff55250..99e50705225dace0c18e55b8a2fafab16815926c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="MavenProjectsManager"> diff --git a/moodle-assignsubmission-autograde/classes/dto/JobRequestDTO.php b/moodle-assignsubmission-autograde/classes/dto/JobRequestDTO.php index baf5c2d536856011843632fd871ef617c25755a4..06bd3f7dc15613199831db20b3ac6b6ed2534e38 100644 --- a/moodle-assignsubmission-autograde/classes/dto/JobRequestDTO.php +++ b/moodle-assignsubmission-autograde/classes/dto/JobRequestDTO.php @@ -36,10 +36,6 @@ defined('MOODLE_INTERNAL') || die(); */ final class JobRequestDTO { - /** @var int ??? */ - public int $userid; - /** @var int ??? */ - public int $courseid; /** @var int ??? */ public int $assignmentid; /** @var int ??? */ @@ -52,16 +48,12 @@ final class JobRequestDTO { /** * ??? - * @param $userid ??? - * @param $courseid ??? * @param $assignmentid ??? * @param $assignctxid ??? * @param $submissionid ??? * @param $image_name ??? */ - public function __construct($userid, $courseid, $assignmentid, $assignctxid, $submissionid, $image_name){ - $this->userid = $userid; - $this->courseid = $courseid; + public function __construct($assignmentid, $assignctxid, $submissionid, $image_name){ $this->assignmentid = $assignmentid; $this->assignctxid = $assignctxid; $this->submissionid = $submissionid; diff --git a/moodle-assignsubmission-autograde/classes/dto/UploadCredentialsDTO.php b/moodle-assignsubmission-autograde/classes/dto/UploadCredentialsDTO.php index 66e37f06a46d84b703d182743a30724c51e17d2f..3e1dafa92d2569a45eaa2a33c3826d3ba1f72887 100644 --- a/moodle-assignsubmission-autograde/classes/dto/UploadCredentialsDTO.php +++ b/moodle-assignsubmission-autograde/classes/dto/UploadCredentialsDTO.php @@ -36,9 +36,6 @@ defined('MOODLE_INTERNAL') || die(); */ final class UploadCredentialsDTO { - /** @var int ??? */ - public int $courseid; - /** @var int ??? */ public int $assignmentid; @@ -49,12 +46,10 @@ final class UploadCredentialsDTO { /** * ??? - * @param $courseid ??? * @param $assignmentid ??? * @param $assignctxid ??? */ - public function __construct($courseid, $assignmentid, $assignctxid){ - $this->courseid = $courseid; + public function __construct($assignmentid, $assignctxid){ $this->assignmentid = $assignmentid; $this->assignctxid = $assignctxid; } diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php b/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php index 84a5ab38be092817d4fee56eb2dd34af299e5795..f451d17929c8f0a43076a4897507cafee0aaad33 100644 --- a/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php +++ b/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php @@ -59,7 +59,6 @@ final class autograde_download_credentials extends external_api { */ public static function download_credentials_parameters(): external_function_parameters { return new external_function_parameters([ - 'courseid' => new external_value(PARAM_INT, 'Course ID'), 'assignmentid' => new external_value(PARAM_INT, 'Assignment ID'), 'assignctxid' => new external_value(PARAM_INT, "Assignment's context ID") ]); @@ -73,11 +72,13 @@ final class autograde_download_credentials extends external_api { * @throws coding_exception ??? * @throws moodle_exception ??? */ - public static function download_credentials($courseid, $assignmentid, $assignctxid): object { - global $USER; + public static function download_credentials($assignmentid, $assignctxid): object { + global $DB, $USER; + // HR : Fetch the course's id from the assignment definition + $course_id = $DB->get_field('assign', 'course', array('id' => $assignmentid)); // HR : Check the necessary capabilities require_capability('mod/assign/submission/autograde:read_credentials', - context_course::instance($courseid), $USER->id, false); + context_course::instance($course_id), $USER->id, false); $fs = get_file_storage(); $files = $fs->get_area_files( diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php b/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php index ccb4f1b64d7f17777f02551d36005d5337be979a..80da93c58025c6a2bb892ce49aa8a287f78dc8d3 100644 --- a/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php +++ b/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php @@ -59,7 +59,6 @@ final class autograde_download_submission extends external_api { */ public static function download_submission_parameters() { return new external_function_parameters([ - 'courseid' => new external_value(PARAM_INT, 'Course ID'), 'submissionid' => new external_value(PARAM_INT, 'Submission ID'), 'assignctxid' => new external_value(PARAM_INT, "Assignment's context ID") ]); @@ -67,17 +66,20 @@ final class autograde_download_submission extends external_api { /** * ??? - * @param $courseid ??? * @param $submissionid ??? * @return stdClass ??? * @throws coding_exception ??? * @throws moodle_exception ??? */ - public static function download_submission($courseid, $submissionid, $assignctxid): stdClass { - global $USER; + public static function download_submission($submissionid, $assignctxid): stdClass { + global $DB, $USER; + // HR : Fetch the assignment's and course's ids linked to the assignment + $assignment_id = $DB->get_field('assign_submission', 'assignment', array('id' => $submissionid)); + $course_id = $DB->get_field('assign', 'course', array('id' => $assignment_id)); + // HR : Check if the user has the necessary capabilities require_capability('mod/assign/submission/autograde:read_submission', - context_course::instance($courseid), $USER->id, false); + context_course::instance($course_id), $USER->id, false); // HR : Retrieve the files associated with the specified area and item $fs = get_file_storage(); diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php b/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php index 4df5363b8b5bd3aeda0d12ac645a507dfcf4300e..d64045d01dad8cf10b6ed1b1c7b19f8dcfec9fb9 100644 --- a/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php +++ b/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php @@ -62,10 +62,7 @@ final class autograde_upload_feedback extends external_api { */ public static function upload_feedback_parameters() { return new external_function_parameters([ - 'userid' => new external_value(PARAM_INT, 'The user ID'), - 'courseid' => new external_value(PARAM_INT, 'The user ID'), - 'assignmentid' => new external_value(PARAM_INT, 'The user ID'), - 'assignctxid' => new external_value(PARAM_INT, 'The user ID'), + 'assignctxid' => new external_value(PARAM_INT, "assign context's id"), 'submissionid' => new external_value(PARAM_INT, 'The user ID'), 'grade' => new external_value(PARAM_INT, 'The user ID'), 'feedback' => new external_value(PARAM_TEXT, 'The feedback message'), @@ -74,9 +71,6 @@ final class autograde_upload_feedback extends external_api { /** * ??? - * @param $userid ??? - * @param $courseid ??? - * @param $assignmentid ??? * @param $submissionid ??? * @param $grade ??? * @param $feedback ??? @@ -84,31 +78,28 @@ final class autograde_upload_feedback extends external_api { * @throws dml_exception ??? * @throws required_capability_exception ??? */ - public static function upload_feedback ( - $userid, $courseid, - $assignmentid, $assignctxid, $submissionid, - $grade, $feedback - ) { - - global $USER, $DB; - + public static function upload_feedback($assignctxid, $submissionid, $grade, $feedback) { + global $DB, $USER; + // HR : Fetch all the necessary ids to upload correctly the feedback + $assignment_id = $DB->get_field('assign_submission', 'assignment', array('id' => $submissionid)); + $user_id = $DB->get_field('assign_submission', 'userid', array('id' => $submissionid)); + $course_id = $DB->get_field('assign', 'course', array('id' => $assignment_id)); + + // HR : Check the necessary capabilities are present require_capability('mod/assign/submission/autograde:update_feedback', - context_course::instance($courseid), $USER->id, false); - - // HR : Fetch the metadata of the submission - $submission = $DB->get_record('assign_submission', array('id' => $submissionid)); + context_course::instance($course_id), $USER->id, false); - $grade_item = grade_item::fetch(['itemmodule' => 'assign', 'iteminstance' => $assignmentid]); + $grade_item = grade_item::fetch(['itemmodule' => 'assign', 'iteminstance' => $assignment_id]); if(!$grade_item){ throw new coding_exception("grade_item not found"); } $gradegradedata = new stdClass(); $gradegradedata->itemid = $grade_item->id; - $gradegradedata->userid = $submission->userid; + $gradegradedata->userid = $user_id; $gradegradedata->rawgrade = $grade; $gradegradedata->finalgrade = $grade; - $gradegradedata->usermodified = $userid; + $gradegradedata->usermodified = $user_id; $gradegradedata->hidden = 0; $gradegradedata->timecreated = time(); $gradegradedata->timemodified = time(); @@ -118,7 +109,7 @@ final class autograde_upload_feedback extends external_api { // HR : Update the grade in the gradebook grade_update( 'mod/assign', - $courseid, + $course_id, 'mod', 'assign', $grade_item->iteminstance, @@ -133,7 +124,7 @@ final class autograde_upload_feedback extends external_api { 'filearea' => ASSIGNSUBMISSION_AUTOGRADE_FEEDBACK_FILEAREA, 'itemid' => $submissionid, 'filepath' => '/', - 'filename' => 'feedback-'. $assignmentid . '-' .$submissionid.'.json' + 'filename' => 'feedback-'. $assignment_id . '-' .$submissionid.'.json' ); get_file_storage()->create_file_from_string($file_info, $feedback); return true; diff --git a/moodle-assignsubmission-autograde/locallib.php b/moodle-assignsubmission-autograde/locallib.php index f8cae1270c47b319130d75fa2708ef8d22bcbfb5..d510f5d7b88947a9c32c5a7a8d6efd51529965a9 100644 --- a/moodle-assignsubmission-autograde/locallib.php +++ b/moodle-assignsubmission-autograde/locallib.php @@ -139,7 +139,6 @@ final class assign_submission_autograde extends assign_submission_plugin { */ public function save_settings(stdClass $data): bool { // HR : Store the Docker image in the configuration - global $COURSE, $DB; $this->set_config(self::setting_docker_image_id, $data->docker_image); // HR : Store the WebService API_KEY in the configuration setting $this->set_config(self::setting_webservice_key_id, $data->webservice_key); @@ -159,7 +158,6 @@ final class assign_submission_autograde extends assign_submission_plugin { $autograde_webservice = new autograde_webservice($this->autograde_api_key()); $request = new UploadCredentialsDTO( - $COURSE->id, $this->assignment->get_instance()->id, $this->assignment->get_context()->id ); @@ -261,7 +259,6 @@ final class assign_submission_autograde extends assign_submission_plugin { * @throws dml_exception ??? */ public function save(stdClass $submissionorgrade, stdClass $data): bool { - global $USER, $COURSE; // HR : Save the files (before, they were marked as Draft) // HR : Without this step, the download_submission webservice won't work @@ -279,8 +276,6 @@ final class assign_submission_autograde extends assign_submission_plugin { // HR : Request from autograde to grade the submission $request = new JobRequestDTO( - $USER->id, - $COURSE->id, $this->assignment->get_instance()->id, $this->assignment->get_context()->id, $submissionorgrade->id, diff --git a/moodle-assignsubmission-autograde/settings.php b/moodle-assignsubmission-autograde/settings.php index 35c8e8f76ff9abbabb11db2ebf6e6d5cd1636cfc..92894304147c8937be4c680d1089b7bab8b323cd 100644 --- a/moodle-assignsubmission-autograde/settings.php +++ b/moodle-assignsubmission-autograde/settings.php @@ -25,6 +25,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__ . '/lib.php'); + $settings->add( new admin_setting_configtext("assignsubmission_autograde/service_url", get_string('admin_setting_url', ASSIGNSUBMISSION_AUTOGRADE_COMPONENT_NAME), diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/KubernetesConfig.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/KubernetesConfig.java index 01d92938931f5fefcbc2e36063e7649e5e595b87..3e7385b4a7411d351781eb6b76b44a8781eb7d55 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/KubernetesConfig.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/KubernetesConfig.java @@ -5,11 +5,19 @@ import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * Configuration and setup of the underlying kubernetes cluster + * + * @author Dixit Sabharwal (dixit.sabharwal@epfl.ch) + * @since 1.0 + */ @Configuration public class KubernetesConfig { + /** Provide a client to communicate to the underlying kubernetes cluster */ @Bean public KubernetesClient kubernetesClient() { return new KubernetesClientBuilder().build(); } + } diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/SecurityConfig.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/SecurityConfig.java index 8db96b3ddf255815a7497551812ade10cbd4a5cd..934c58e74a6536506085fb729e1450a4c2af4c86 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/SecurityConfig.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/SecurityConfig.java @@ -11,24 +11,24 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; /** - * ??? + * Security configuration and setup of the autograde service. * * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 */ @Configuration @EnableWebSecurity public class SecurityConfig { - /** ??? */ - private final ApiKeyAuthenticationFilter apiKeyAuthFilter; + /** autograde custom API-KEY authentication filter */ + private final ApiKeyAuthenticationFilter auth; /** - * ??? - * @param apiKeyAuthFilter ??? + * @param auth autograde authentication filter */ @Autowired - public SecurityConfig(ApiKeyAuthenticationFilter apiKeyAuthFilter) { - this.apiKeyAuthFilter = apiKeyAuthFilter; + public SecurityConfig(ApiKeyAuthenticationFilter auth) { + this.auth = auth; } /** @@ -41,7 +41,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http // HR : Add the ApiKeyAuthentication filter - .addFilterAfter(apiKeyAuthFilter, BasicAuthenticationFilter.class) + .addFilterAfter(auth, BasicAuthenticationFilter.class) // HR : Disable CSRF .csrf(AbstractHttpConfigurer::disable) // HR : Configure the end points authorizations diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/FeedbackController.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/FeedbackController.java new file mode 100644 index 0000000000000000000000000000000000000000..037955fd64645fe0c874c837f22eec7da85e061a --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/FeedbackController.java @@ -0,0 +1,82 @@ +package ch.epfl.cs107.grading.moodle.api.v1.controller; + +import ch.epfl.cs107.grading.moodle.api.v1.dto.UploadFeedbackDTO; +import ch.epfl.cs107.grading.moodle.api.v1.service.MoodleWebService; +import ch.epfl.cs107.grading.moodle.api.v1.service.IntegrityService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * REST Controller to upload an autograde feedback to Moodle. + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 + */ +@RestController +@RequestMapping("/api/v1/feedback") +public final class FeedbackController { + + private final Logger logger = LoggerFactory.getLogger(FeedbackController.class); + + /** Service to generate and check the integrity of the requests */ + private final IntegrityService integrity; + + /** Service to communicate with Moodle */ + private final MoodleWebService moodle; + + /** + * @param moodle service to use to communicate with Moodle. + * @param integrity service to use for the integrity checks. + */ + @Autowired + public FeedbackController(MoodleWebService moodle, IntegrityService integrity) { + this.moodle = moodle; + this.integrity = integrity; + } + + /** + * Endpoint to upload an autograde feedback to Moodle + * + * @param submissionid ID of the submission on Moodle + * @param signature HMAC of the request as generated by the autograde-service + * @param feedback Autograde compatible feedback + * @since 1.0 + * @apiNote This end point should only be called by the autograde-service itself + */ + @PostMapping("/upload") + public ResponseEntity<?> submitGrade( + @RequestParam int assignctxid, + @RequestParam int submissionid, + @RequestParam String signature, + @RequestBody UploadFeedbackDTO feedback + ) { + logger.info("Received an 'upload feedback' request for submission with id {}", submissionid); + // HR : Check the integrity of the request + if (!integrity.check(signature, submissionid)) { + logger.error("Integrity check failed for 'upload feedback' request for submission {}", submissionid); + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("The provided signature is not correct !"); + } + logger.info("Integrity check for 'upload feedback' request for submission {} was successful", submissionid); + // HR : Upload the grade and feedback to Moodle + try { + var res = moodle.upload_feedback(assignctxid, submissionid, feedback.getGrade(), feedback.getFeedback().toJson()); + if (res.statusCode() != HttpStatus.OK.value()) { + throw new Exception("Moodle request returned with status: " + res.statusCode() + " " + res.body()); + } else { + // HR : Request was successful + logger.info("Feedback was successfully stored on Moodle for submission with id {}", submissionid); + return ResponseEntity.ok().build(); + } + } catch (Exception e) { + logger.error("Failed to upload the grade and feedback to Moodle for submission with id {}", submissionid, e); + return ResponseEntity.internalServerError().build(); + } + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/GradingController.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/GradingController.java index c4f41526d9384dae0b7213f0f7cc5215e229baed..1ecf869bb2c36c9028f58baaff32c92ae641cf90 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/GradingController.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/GradingController.java @@ -1,126 +1,66 @@ package ch.epfl.cs107.grading.moodle.api.v1.controller; import ch.epfl.cs107.grading.moodle.api.v1.dto.GradeRequestDTO; -import ch.epfl.cs107.grading.moodle.api.v1.dto.UploadFeedbackDTO; -import ch.epfl.cs107.grading.moodle.api.v1.service.KubernetesJobService; -import ch.epfl.cs107.grading.moodle.api.v1.service.MoodleWebService; -import ch.epfl.cs107.grading.moodle.api.v1.service.SubmissionIntegrityService; +import ch.epfl.cs107.grading.moodle.api.v1.service.KubernetesService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; /** - * Handles grading requests from moodle and submit grades and feedback to - * moodle. + * REST Controller to request an autograde grading for a given submission * * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 */ @RestController @RequestMapping("/api/v1/grading") -public class GradingController { +public final class GradingController { private final Logger logger = LoggerFactory.getLogger(GradingController.class); - /** - * Send requests to moodle - */ - private final MoodleWebService moodleWebService; - - /** - * Trigger jobs on kubernetes cluster - */ - private final KubernetesJobService kubernetesJobService; + /** Service to communicate with the k8s cluster */ + private final KubernetesService k8s; /** - * Check the integrity of the result submission requests + * @param k8s service to use to communicate with the k8s cluster. */ - private final SubmissionIntegrityService integrity; - - @Autowired - public GradingController(MoodleWebService moodleWebService, - KubernetesJobService kubernetesJobService, - SubmissionIntegrityService integrity) { - this.moodleWebService = moodleWebService; - this.kubernetesJobService = kubernetesJobService; - this.integrity = integrity; + public GradingController(KubernetesService k8s) { + this.k8s = k8s; } /** - * Handles grading requests from moodle. Creates a grading job on the kubernetes - * cluster. + * Endpoint to request the grading of an autograde compatible submission * - * @param request ??? - * @return ??? + * @param request Grading request with the necessary information + * @return HTTP.OK with the job name in the body if successful, + * HTTP.INTERNAL_ERROR otherwise + * @since 1.0 */ @PostMapping("/grade") public ResponseEntity<?> grade(@RequestBody GradeRequestDTO request) { - - logger.info("Grading request received: {}", request.toString()); - + logger.info("Received request to grade submission with id {}", request.getSubmissionid()); try { - var jobName = kubernetesJobService.createJob( - request.getUserid(), request.getCourseid(), - request.getAssignmentid(), request.getSubmissionid(), - request.getAssignctxid(), request.getImage_name()); + // TODO HR : createJob should take the request as parameter + var jobName = k8s.createJob(request.getAssignmentid(), + request.getSubmissionid(), + request.getAssignctxid(), + request.getImage_name() + ); - logger.info("Grading job created: {}", jobName); - return ResponseEntity.ok(jobName); + logger.info("Grading submission with id {} will be carried out by job with name {}", + request.getSubmissionid(), jobName); + return ResponseEntity + .ok(jobName); } catch (RuntimeException e) { - logger.error("Failed to create grading job", e); - return ResponseEntity.internalServerError().body("Failed to create grading job"); - } - } - - /** - * Handles requests from grading job to submit grades and feedback. Uploads - * grades and feedback to moodle. - * - * @param userid ??? - * @param courseid ??? - * @param assignmentid ??? - * @param submissionid ??? - * @param signature ??? - * @param feedback ??? - */ - @PostMapping("/result") - public ResponseEntity<?> submitGrade( - @RequestParam int userid, - @RequestParam int courseid, - @RequestParam int assignmentid, - @RequestParam int assignctxid, - @RequestParam int submissionid, - @RequestParam String signature, - @RequestBody UploadFeedbackDTO feedback - ) { - if (!integrity.check(signature, courseid, submissionid)) { - logger.error("The provided signature to submit grade is not correct !"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("The provided signature is not correct !"); - } - - logger.info("Grade/feedback submitted by job: userid={} courseid={} assignmentid={} submissionid={}", - userid, courseid, assignmentid, submissionid - ); - try { - // HR : Upload grade and feedback to moodle - var res = moodleWebService.uploadAutoGradeFeedback( - userid, courseid, assignmentid, assignctxid, submissionid, - feedback.getGrade(), feedback.getFeedback().toJson()); - if (res.statusCode() != HttpStatus.OK.value()) { - throw new Exception("Moodle request returned with status: " + res.statusCode() + " " + res.body()); - } - } catch (Exception e) { - logger.error("Failed to upload grade and feedback to moodle: userid={} courseid={} assignmentid={} submissionid={}", - userid, courseid, assignmentid, submissionid, - e - ); + logger.error("Failed to create a grading job to grade submission with id {}", + request.getSubmissionid(), e); + return ResponseEntity + .internalServerError() + .body("Failed to create grading job"); } - logger.info("Feedback submitted successfully: userid={} courseid={} assignmentid={} submissionid={}", - userid, courseid, assignmentid, submissionid); - return ResponseEntity.ok().build(); } } diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/PingController.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/PingController.java index f9104bb578f28448db7d3a81ff4158f241d5672b..56346c2c64e98a42770b9d3f3083f1bda48a27bc 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/PingController.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/PingController.java @@ -7,36 +7,40 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** - * ??? + * REST Controller for '/api/v1/ping' and provide end points + * to ping the autograde-service in different modes. * * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 */ @RestController @RequestMapping("/api/v1/ping") -public class PingController { +public final class PingController { - private Logger logger = LoggerFactory.getLogger(PingController.class); + private final Logger logger = LoggerFactory.getLogger(PingController.class); /** - * ??? - * - * @return ??? + * Endpoint to ping the service without authentication + * @return greetings from the service + * @since 1.0 */ @GetMapping("/no-auth") public String pingWithoutAuth() { - logger.info("Received a ping without auth"); - return "Hello from Spring boot - No Auth mode"; + logger.info("Received a ping from an unauthenticated source"); + return "Hello and welcome to the autograde-service API. " + + "You have successfully pinged the service in no-auth mode."; } /** - * ??? - * - * @return ??? + * Endpoint to ping the service when authenticated + * @return greetings from the service + * @since 1.0 */ @GetMapping("/auth") public String pingWithAuth() { - logger.info("Received a ping"); - return "Hello from Spring boot - Auth mode"; + logger.info("Received a ping from an authenticated source"); + return "Hello and welcome to the autograde-service API. " + + "You have successfully pinged the service in auth mode."; } } diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/RegistryCredentialsController.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/RegistryCredentialsController.java index 3dfa6abbdd03b5d622271b87f387012babc44031..e08da85de6778f978a4873922711afc9f1fefaab 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/RegistryCredentialsController.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/RegistryCredentialsController.java @@ -1,7 +1,7 @@ package ch.epfl.cs107.grading.moodle.api.v1.controller; import ch.epfl.cs107.grading.moodle.api.v1.dto.UploadCredentialsDTO; -import ch.epfl.cs107.grading.moodle.api.v1.service.KubernetesJobService; +import ch.epfl.cs107.grading.moodle.api.v1.service.KubernetesService; import ch.epfl.cs107.grading.moodle.api.v1.service.MoodleWebService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,13 +12,13 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Arrays; import java.util.Base64; /** - * ??? + * REST Controller to manage the credentials stored in the cluster. * * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 */ @RestController @RequestMapping("/api/v1/registry/credentials") @@ -26,54 +26,43 @@ public final class RegistryCredentialsController { private final Logger logger = LoggerFactory.getLogger(RegistryCredentialsController.class); - /** - * ??? - */ - private final KubernetesJobService k8s; + /** Service to communicate with the k8s cluster */ + private final KubernetesService k8s; - /** - * ??? - */ + /** Service to communicate with Moodle */ private final MoodleWebService moodle; /** - * ??? - * - * @param k8s ??? + * @param k8s service to use to communicate with the k8s cluster. + * @param moodle service to use to communicate with Moodle. */ @Autowired - public RegistryCredentialsController(KubernetesJobService k8s, MoodleWebService moodle) { + public RegistryCredentialsController(KubernetesService k8s, MoodleWebService moodle) { this.k8s = k8s; this.moodle = moodle; } /** - * ??? - * - * @return ??? + * Endpoint to store the image registry credentials in the cluster. + * @return HTTP.OK if successful, HTTP.INTERNAL_ERROR if not. + * @since 1.0 */ @PostMapping("/upload") public ResponseEntity<?> uploadCredentials(@RequestBody UploadCredentialsDTO credentials) { - logger.info("Received credentials upload request for course {} and assignment {}", - credentials.getCourseid(), credentials.getAssignmentid()); - logger.debug("Credentials for course {} and assignment {} are {}", - credentials.getCourseid(), credentials.getAssignmentid(), credentials); + logger.info("Received request to store the credentials for assignment {}", credentials.getAssignmentid()); //try (var dockerconfigjson = moodle.download_credentials(credentials.getCourseid(), credentials.getAssignmentid(), credentials.getAssignctxid())) { try { k8s.tryCreateImagePullSecret( //new String(dockerconfigjson.readAllBytes()), new String(Base64.getDecoder().decode(credentials.getContent())), - credentials.getCourseid(), credentials.getAssignmentid()); - logger.info("Successfully created ImagePullSecret for course {} and assignment {}", - credentials.getCourseid(), credentials.getAssignmentid()); - return ResponseEntity.ok().body(null); + logger.info("Successfully stored the credentials for assignment {}", credentials.getAssignmentid()); + return ResponseEntity.ok().build(); } catch (Exception e) { - logger.error("Failed to create ImagePullSecret for course {} and assignment {}", - credentials.getCourseid(), credentials.getAssignmentid(), e); - return ResponseEntity.internalServerError().body("Failed to store credentials"); + logger.error("Failed to store the credentials for assignment {}", credentials.getAssignmentid(), e); + return ResponseEntity.internalServerError().body("Failed to store credentials in the cluster"); } } diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/SubmissionController.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/SubmissionController.java index a5e880423803554dfe7be70ff169fd6b64bcf313..8a43f5bccdfa13b975de1bb7ca2d54d9a4eb2582 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/SubmissionController.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/SubmissionController.java @@ -1,7 +1,7 @@ package ch.epfl.cs107.grading.moodle.api.v1.controller; import ch.epfl.cs107.grading.moodle.api.v1.service.MoodleWebService; -import ch.epfl.cs107.grading.moodle.api.v1.service.SubmissionIntegrityService; +import ch.epfl.cs107.grading.moodle.api.v1.service.IntegrityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -14,77 +14,79 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.io.IOException; -import java.net.URISyntaxException; - import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE; /** - * ??? + * REST Controller to fetch an autograde compatible submission from Moodle. * * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 */ @RestController @RequestMapping("/api/v1/submission") -public class SubmissionController { +public final class SubmissionController { private final Logger logger = LoggerFactory.getLogger(SubmissionController.class); - /** - * ??? - */ + /** Service to communicate with Moodle */ private final MoodleWebService moodle; - private final SubmissionIntegrityService integrity; + /** Service to generate and check the integrity of the requests */ + private final IntegrityService integrity; /** - * ??? - * - * @param moodle ??? + * @param moodle service to use to communicate with Moodle. + * @param integrity service to use for the integrity checks. */ @Autowired - public SubmissionController(MoodleWebService moodle, SubmissionIntegrityService integrity) { + public SubmissionController(MoodleWebService moodle, IntegrityService integrity) { this.moodle = moodle; this.integrity = integrity; } - /** - * ??? + * Endpoint to download a given autograde compatible submission from Moodle. * - * @return ??? + * @param submissionid ID of the submission in Moodle + * @param assignctxid ID of the context in Moodle + * @param signature HMAC of the request as generated by the autograde-service + * @return HTTP.OK with the submission if successful, + * HTTP.UNAUTHORIZED if the HMAC is wrong + * HTTP.INTERNAL_ERROR otherwise. + * @since 1.0 + * @apiNote This end point should only be called by the autograde-service itself */ @GetMapping("/download") - public ResponseEntity<?> download(@RequestParam int courseid, - @RequestParam int submissionid, + public ResponseEntity<?> download(@RequestParam int submissionid, @RequestParam int assignctxid, @RequestParam String signature) { - - logger.info("Received submission download request for course {} and submission {}", - courseid, submissionid); + logger.info("Received a 'download submission' request for submission {}", submissionid); // HR : Check the integrity of the signature - if (!integrity.check(signature, courseid, submissionid)) - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("The provided signature is not correct !"); - + if (!integrity.check(signature, submissionid)){ + logger.error("Integrity check failed for 'download submission' request for submission {}", submissionid); + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("The provided signature is not correct !"); + } + logger.info("Integrity check for 'download submission' request for submission {} was successful", submissionid); // HR : Serve the actual file, the integrity of the signature was checked - try (var file = moodle.download_submission(courseid, submissionid, assignctxid)) { + try (var file = moodle.download_submission(submissionid, assignctxid)) { // HR : Prepare the headers var headers = new HttpHeaders(); headers.add(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE); - - // HR : Prepare the body - var body = new InputStreamResource(file); - - logger.info("Submission download request for course {} and submission {} served successfully", - courseid, submissionid); // HR : Build response - return ResponseEntity.ok().headers(headers).body(body); - } catch (URISyntaxException | InterruptedException | IOException e) { - logger.error("An error occurred while downloading the submission courseid={}, submissionid={} from moodle", - courseid, submissionid, e); - return ResponseEntity.internalServerError().body("An error occurred while downloading the submission !"); + logger.info("'download submission' request for submission with id {} is served successfully", submissionid); + return ResponseEntity + .ok() + .headers(headers) + .body(new InputStreamResource(file)); + } catch (Exception e) { + logger.error("Failed to fetch the submission with id {} from Moodle", submissionid, e); + return ResponseEntity + .internalServerError() + .body("An error occurred while downloading the submission !"); } } diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/GradeRequestDTO.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/GradeRequestDTO.java index 6aa6dcb8d4377254b792a5756c6a4d101e9f253b..4df9156729dabb58f24efbc47ab1e00eb4cd8d9c 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/GradeRequestDTO.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/GradeRequestDTO.java @@ -8,12 +8,6 @@ import lombok.Data; @Data public class GradeRequestDTO { - /** ??? */ - private final int userid; - - /** ??? */ - private final int courseid; - /** ??? */ private final int assignmentid; diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/UploadCredentialsDTO.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/UploadCredentialsDTO.java index e9e1970bd3d5331fe39742f70a6b51ed5463039f..a9a7a80f7337ba19f03077a2a5bfcc43b2d7d791 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/UploadCredentialsDTO.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/UploadCredentialsDTO.java @@ -10,9 +10,6 @@ import lombok.Data; @Data public final class UploadCredentialsDTO { - /** ???? */ - private final int courseid; - private final int assignmentid; private final int assignctxid; diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/IntegrityService.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/IntegrityService.java new file mode 100644 index 0000000000000000000000000000000000000000..0eb6ff03794ec2085045ff5728c0db3cb84b02a8 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/IntegrityService.java @@ -0,0 +1,83 @@ +package ch.epfl.cs107.grading.moodle.api.v1.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.isNull; + +/** + * Service to compute and check the HMAC of a request + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 + */ +@Service +public final class IntegrityService { + + /** Algorithm to use when hashing */ + private static final String HMAC_ALGORITHM = "HmacSHA256"; + + private final Logger logger = LoggerFactory.getLogger(IntegrityService.class); + + /** Randomly generated secret for hashing */ + private static final SecretKey secret; + + // HR : Generate the secret randomly + static { + try { + secret = KeyGenerator.getInstance(HMAC_ALGORITHM).generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Generate the HMAC for a given submission ID and course ID + * @param id ID to hash + * @return the corresponding HMAC, null if the generation failed + */ + public String generate(int id){ + logger.info("Generating integrity HMAC for id {}", id); + // HR : Generate a unique string with both ids + var data = String.format("##%d##", id); + try { + var mac = Mac.getInstance(HMAC_ALGORITHM); + // HR : Initialize the MAC with the secret + mac.init(secret); + // HR : Append the data to the MAC + var macBytes = mac.doFinal(data.getBytes(UTF_8)); + // HR : Return a base64 encoding of the MAC + return Base64.getEncoder().encodeToString(macBytes); + } catch (Exception e) { + logger.error("HMAC generation failed for request with id {}", id, e); + return null; + } + } + + /** + * Check if the provided HMAC is valid + * @param hmac HMAC to verify + * @param id id to verify + * @return true if the HMAC is valid, false otherwise + */ + public boolean check(String hmac, int id){ + logger.info("Checking HMAC integrity for HMAC = {}", hmac); + var generated = generate(id); + if (isNull(generated)){ + logger.error("Cannot verify HMAC = {} - HMAC generation failed", hmac); + return false; + } else { + logger.info("HMAC generation for verification was successful. Comparing both HMACs."); + return generated.equals(hmac); + } + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesJobService.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesService.java similarity index 78% rename from moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesJobService.java rename to moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesService.java index 2af62194da97bde5366308bda26ae58651a1b3fa..4ed67d31d877b64db9b41278654ad44e46a7f68a 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesJobService.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesService.java @@ -20,62 +20,67 @@ import java.util.Optional; import java.util.UUID; /** - * ??? + * Service to communicate with the k8s cluster. * * @author Dixit Sabharwal (dixit.sabharwal@epfl.ch) * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0 */ @Service -public final class KubernetesJobService { +public final class KubernetesService { - /** - * ??? - */ - private final Logger logger = LoggerFactory.getLogger(KubernetesJobService.class); + private final Logger logger = LoggerFactory.getLogger(KubernetesService.class); - /** - * ??? - */ - private final KubernetesClient kubernetesClient; + /** Client to communicate with the underlying k8s cluster */ + private final KubernetesClient k8sClient; - /** - * ??? - */ - private final SubmissionIntegrityService subIntegrity; + /** Service to generate and check the integrity of the requests */ + private final IntegrityService integrity; + + /** URL to access the grading service itself */ + private final String gradingServiceName; - @Value("${grading.service.name}") - private String gradingServiceName; - @Value("${server.port}") - private int gradingServicePort; + /** Port to access the grading service itself */ + private final int gradingServicePort; + // TODO HR : This is not a 100% correct, we will have multiple namespaces (staging, production) private final String GRADING_SERVICE_NAMESPACE = "grading-service"; - @Autowired - public KubernetesJobService(KubernetesClient kubernetesClient, SubmissionIntegrityService subIntegrity) { - this.kubernetesClient = kubernetesClient; - this.subIntegrity = subIntegrity; + /** + * @param k8sClient client to use to access the underlying k8s cluster + * @param integrity service to use for the integrity checks. + * @param serviceName URL to access the autograde-service itself + * @param servicePort Port to access the autograde-service itself + */ + public KubernetesService( + @Autowired KubernetesClient k8sClient, + @Autowired IntegrityService integrity, + @Value("${grading.service.name}") String serviceName, + @Value("${server.port}") int servicePort) { + this.k8sClient = k8sClient; + this.integrity = integrity; + this.gradingServiceName = serviceName; + this.gradingServicePort = servicePort; } /** * ??? * - * @param userid ??? - * @param courseid ??? * @param assignmentid ??? * @param submissionid ??? * @param imageName ??? * @return ??? */ - public String createJob(int userid, int courseid, int assignmentid, int submissionid, int assignctxid, String imageName) + public String createJob(int assignmentid, int submissionid, int assignctxid, String imageName) throws RuntimeException { // Generate the job name var jobName = generate_job_name(); // Check if imagePullSecret exists - var imagePullSecret = getSecret(generate_imagePullSecret_name(courseid, assignmentid)); + var imagePullSecret = getSecret(generate_imagePullSecret_name(assignmentid)); if (imagePullSecret.isEmpty()) { - logger.error("ImagePullSecret for course {}, assignment {} does not exist. Tried using image {}.", courseid, assignmentid, imageName); + logger.error("ImagePullSecret for assignment {} does not exist. Tried using image {}.", assignmentid, imageName); throw new RuntimeException("Docker credentials do not exist."); } @@ -83,21 +88,25 @@ public final class KubernetesJobService { var volume = prepare_job_volume(jobName); // Create an InitContainer to download the zip file into the ephemeral volume - var initContainer = prepare_init_container(volume, download_submission_command(courseid, submissionid, assignctxid)); + var initContainer = prepare_init_container(volume, download_submission_command(submissionid, assignctxid)); // Create container that will run the grading script var gradingContainer = prepare_grading_container(volume, imageName); // Create a ReportingContainer to report the grading results - var reportingContainer = prepare_feedback_container(volume, upload_feedback_command(userid, courseid, assignmentid, assignctxid, submissionid)); + var reportingContainer = prepare_feedback_container(volume, upload_feedback_command(assignctxid, submissionid)); // Define job - var job = create_job_object(jobName, volume, generate_imagePullSecret_name(courseid, submissionid), - initContainer, gradingContainer, reportingContainer, - userid, courseid, assignmentid, submissionid); + var job = create_job_object(jobName, volume, generate_imagePullSecret_name(submissionid), + initContainer, gradingContainer, reportingContainer, assignmentid, submissionid); // Create job try { logger.info("Creating job {}", jobName); - kubernetesClient.batch().v1().jobs().inNamespace(GRADING_SERVICE_NAMESPACE).resource(job).create(); + k8sClient.batch() + .v1() + .jobs() + .inNamespace(GRADING_SERVICE_NAMESPACE) + .resource(job) + .create(); } catch (KubernetesClientException e) { logger.error("Failed to create job {}", jobName, e); throw new RuntimeException("Failed to create job"); @@ -109,7 +118,7 @@ public final class KubernetesJobService { private Optional<Secret> getSecret(String secretName) { Secret secret = null; try { - secret = kubernetesClient.secrets().inNamespace(GRADING_SERVICE_NAMESPACE).withName(secretName).get(); + secret = k8sClient.secrets().inNamespace(GRADING_SERVICE_NAMESPACE).withName(secretName).get(); } catch (KubernetesClientException e) { logger.error("Failed trying to read existing secret {}", secretName, e); throw new RuntimeException("Failed trying to read existing imagePullSecret"); @@ -117,8 +126,8 @@ public final class KubernetesJobService { return Optional.ofNullable(secret); } - public void tryCreateImagePullSecret(String dockerconfigjson, int courseid, int assignmentid) throws RuntimeException { - String secretName = generate_imagePullSecret_name(courseid, assignmentid); + public void tryCreateImagePullSecret(String dockerconfigjson, int assignmentid) throws RuntimeException { + String secretName = generate_imagePullSecret_name(assignmentid); String base64Dockerconfigjson = Base64.getEncoder().encodeToString(dockerconfigjson.getBytes()); // Check if secret exists @@ -132,7 +141,7 @@ public final class KubernetesJobService { logger.info("Secret {} already exists but is different, deleting it", secretName); try { logger.info("Deleting secret {}", secretName); - kubernetesClient.secrets().inNamespace(GRADING_SERVICE_NAMESPACE).withName(secretName).delete(); + k8sClient.secrets().inNamespace(GRADING_SERVICE_NAMESPACE).withName(secretName).delete(); } catch (KubernetesClientException e) { logger.error("Failed to delete secret {}", secretName, e); throw new RuntimeException("Failed to delete imagePullSecret"); @@ -155,7 +164,7 @@ public final class KubernetesJobService { try { logger.info("Creating secret {}", secretName); - kubernetesClient.secrets().inNamespace(GRADING_SERVICE_NAMESPACE).resource(regCred).create(); + k8sClient.secrets().inNamespace(GRADING_SERVICE_NAMESPACE).resource(regCred).create(); } catch (KubernetesClientException e) { logger.error("Failed to create secret {}", secretName, e); throw new RuntimeException("Failed to create imagePullSecret"); @@ -178,12 +187,11 @@ public final class KubernetesJobService { /** * ??? * - * @param courseid ??? * @param assignmentid ??? * @return ??? */ - private String generate_imagePullSecret_name(int courseid, int assignmentid) { - return String.format("regcred-%d-%d", courseid, assignmentid); + private String generate_imagePullSecret_name(int assignmentid) { + return String.format("regcred-%d", assignmentid); } /** @@ -315,8 +323,6 @@ public final class KubernetesJobService { * @param initContainer ??? * @param gradingContainer ??? * @param reportingContainer ??? - * @param userid ??? - * @param courseid ??? * @param assignmentid ??? * @param submissionid ??? * @return ??? @@ -324,7 +330,7 @@ public final class KubernetesJobService { private Job create_job_object( String job_name, Volume volume, String imagePullSecretRef, Container initContainer, Container gradingContainer, Container reportingContainer, - int userid, int courseid, int assignmentid, int submissionid + int assignmentid, int submissionid ) { return new JobBuilder() .withApiVersion("batch/v1") @@ -332,8 +338,6 @@ public final class KubernetesJobService { .withMetadata(new ObjectMetaBuilder() .withName(job_name) .withLabels(Map.of( - "userid", String.valueOf(userid), - "courseid", String.valueOf(courseid), "assignmentid", String.valueOf(assignmentid), "submissionid", String.valueOf(submissionid) )) @@ -355,35 +359,31 @@ public final class KubernetesJobService { /** * ??? * - * @param courseid ??? * @param submissionid ??? * @return ??? */ - private String download_submission_command(int courseid, int submissionid, int assignctxid) { - var mac = URLEncoder.encode(subIntegrity.generate(courseid, submissionid), StandardCharsets.UTF_8); + private String download_submission_command(int submissionid, int assignctxid) { + var mac = URLEncoder.encode(integrity.generate(submissionid), StandardCharsets.UTF_8); return "curl -X GET -H \"API-KEY: $API_KEY\" " + - String.format("\"http://%s:%d/api/v1/submission/download?courseid=%d&submissionid=%d&assignctxid=%d&signature=%s\" ", - gradingServiceName, gradingServicePort, courseid, submissionid, assignctxid, mac) + + String.format("\"http://%s:%d/api/v1/submission/download?submissionid=%d&assignctxid=%d&signature=%s\" ", + gradingServiceName, gradingServicePort, submissionid, assignctxid, mac) + "-o /data/submission.zip"; } /** * ??? * - * @param userid ??? - * @param courseid ??? - * @param assignmentid ??? * @param submissionid ??? * @return ??? */ - private String upload_feedback_command(int userid, int courseid, int assignmentid, int assignctx, int submissionid) { - var mac = URLEncoder.encode(subIntegrity.generate(courseid, submissionid), StandardCharsets.UTF_8); + private String upload_feedback_command(int assignctx, int submissionid) { + var mac = URLEncoder.encode(integrity.generate(submissionid), StandardCharsets.UTF_8); return "curl -X POST -H \"API-KEY: $API_KEY\" -H 'Content-Type: application/json' " + "-d @/data/feedback.json " + - String.format("\"http://%s:%d/api/v1/grading/result?userid=%d&courseid=%d&assignmentid=%d&assignctxid=%d&submissionid=%d&signature=%s\"", - gradingServiceName, gradingServicePort, userid, courseid, assignmentid, assignctx, submissionid, mac); + String.format("\"http://%s:%d/api/v1/feedback/upload?assignctxid=%d&submissionid=%d&signature=%s\"", + gradingServiceName, gradingServicePort, assignctx, submissionid, mac); } } \ No newline at end of file diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/MoodleWebService.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/MoodleWebService.java index 212107c6625efdb9ea2a318629443ddcacafdac2..9a82cd6dcf0cad026a903f89778039208ced6efc 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/MoodleWebService.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/MoodleWebService.java @@ -26,28 +26,28 @@ import static java.net.http.HttpResponse.BodyHandlers.ofString; import static java.nio.charset.StandardCharsets.UTF_8; /** - * ??? + * Service to communicate with Moodle. + * * @author Hamza REMMAL (hamza.remmal@epfl.ch) * @since 1.0.0 */ @Service public final class MoodleWebService { - /** ??? */ + private final Logger logger = LoggerFactory.getLogger(MoodleWebService.class); + + /** Moodle's end-point to receive REST requests */ private final static String MOODLE_WEB_SERVICE_API = "/webservice/rest/server.php"; - /** ??? */ + /** base URL of the Moodle instance to call */ private final String MOODLE_BASE_URL; - /** ??? */ + /** autograde Moodle's access token */ private final String MOODLE_ACCESS_TOKEN; - private final Logger logger = LoggerFactory.getLogger(MoodleWebService.class); - /** - * ??? - * @param url ??? - * @param token ??? + * @param url URL of the Moodle instance to communicate with + * @param token autograde access token to the Moodle instance */ public MoodleWebService(@Value("${moodle.baseurl}")String url, @Value("${moodle.autograde.token}") String token) { this.MOODLE_BASE_URL = url; @@ -96,11 +96,12 @@ public final class MoodleWebService { return client.send(request, handler); } + // ============================================================================================ + // ================================== AUTOGRADE FUNCTIONS ===================================== + // ============================================================================================ + /** - * ??? - * @param userid ??? - * @param courseid ??? - * @param assignmentid ??? + * Upload the feedback for an autograde compatible submission. * @param submissionid ??? * @param grade ??? * @param feedback ??? @@ -109,22 +110,13 @@ public final class MoodleWebService { * @throws IOException ??? * @throws InterruptedException ??? */ - public HttpResponse<?> uploadAutoGradeFeedback( - int userid, - int courseid, - int assignmentid, - int assignctxid, - int submissionid, - int grade, - String feedback - ) throws URISyntaxException, IOException, InterruptedException { + public HttpResponse<?> upload_feedback(int assignctxid, int submissionid, int grade, String feedback) throws URISyntaxException, IOException, InterruptedException { + logger.info("Uploading feedback for submission with id {}", submissionid); + final var FUNCTION_NAME = "mod_assignsubmission_autograde_upload_feedback"; final var params = new HashMap<String, Object>(); // HR : Build the parameters - params.put("userid", userid); - params.put("courseid", courseid); - params.put("assignmentid", assignmentid); params.put("assignctxid", assignctxid); params.put("submissionid", submissionid); params.put("grade", grade); @@ -134,21 +126,19 @@ public final class MoodleWebService { } /** - * ??? - * @param courseid ??? + * Download an autograde compatible submission from Moodle * @param submissionid ??? * @return ??? * @throws URISyntaxException ??? * @throws IOException ??? * @throws InterruptedException ??? */ - public InputStream download_submission(int courseid, int submissionid, int assignctxid) throws URISyntaxException, IOException, InterruptedException { - logger.info("downloading submission {} in course {}", submissionid, courseid); + public InputStream download_submission(int submissionid, int assignctxid) throws URISyntaxException, IOException, InterruptedException { + logger.info("Downloading submission with id {} from Moodle ({})", submissionid, MOODLE_BASE_URL); final var FUNCTION_NAME = "mod_assignsubmission_autograde_download_submission"; final var params = new HashMap<String, Object>(); - params.put("courseid", courseid); params.put("submissionid", submissionid); params.put("assignctxid", assignctxid); @@ -165,21 +155,19 @@ public final class MoodleWebService { } /** - * ??? - * @param courseid ??? + * Download the credentials for an autograde compatible assignment * @param assignmentid ??? * @return ??? * @throws URISyntaxException ??? * @throws IOException ??? * @throws InterruptedException ??? */ - public InputStream download_credentials(int courseid, int assignmentid, int assignctxid) throws URISyntaxException, IOException, InterruptedException { - logger.info("downloading credentials for assignment {} in course {}", assignmentid, courseid); + public InputStream download_credentials(int assignmentid, int assignctxid) throws URISyntaxException, IOException, InterruptedException { + logger.info("Downloading the credentials for assignment with id {}", assignmentid); final var FUNCTION_NAME = "mod_assignsubmission_autograde_download_credentials"; final var params = new HashMap<String, Object>(); - params.put("courseid", courseid); params.put("assignmentid", assignmentid); params.put("assignctxid", assignctxid); diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/SubmissionIntegrityService.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/SubmissionIntegrityService.java deleted file mode 100644 index 1fba9f63150938c06ad7e30def3e9fbfe38609fe..0000000000000000000000000000000000000000 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/SubmissionIntegrityService.java +++ /dev/null @@ -1,69 +0,0 @@ -package ch.epfl.cs107.grading.moodle.api.v1.service; - -import org.springframework.stereotype.Service; - -import javax.crypto.KeyGenerator; -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * ??? - * - * @author Hamza REMMAL (hamza.remmal@epfl.ch) - */ -@Service -public final class SubmissionIntegrityService { - - /** ??? */ - private static final String HMAC_ALGORITHM = "HmacSHA256"; - - /** ??? */ - private static final SecretKey secret; - - static { - try { - secret = KeyGenerator.getInstance(HMAC_ALGORITHM).generateKey(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - /** - * ??? - * @param courseid ??? - * @param submissionid ??? - * @return ??? - */ - public String generate(int courseid, int submissionid){ - // HR : Generate a unique string with both ids - var data = String.format("%d:%d", courseid, submissionid); - try { - var mac = Mac.getInstance(HMAC_ALGORITHM); - // HR : Initialize the MAC with the secret - mac.init(secret); - // HR : Append the data to the MAC - var macBytes = mac.doFinal(data.getBytes(UTF_8)); - // HR : Return a base64 encoding of the MAC - return Base64.getEncoder().encodeToString(macBytes); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } - } - - /** - * ??? - * @param mac ??? - * @param courseid ??? - * @param submissionid ??? - * @return ??? - */ - public boolean check(String mac, int courseid, int submissionid){ - return generate(courseid, submissionid).equals(mac); - } - -}