diff --git a/.github/workflows/autograde-service-tests.yml b/.github/workflows/autograde-service-tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..b861b07ea8a0bce7e71effca0b72295efe504bce --- /dev/null +++ b/.github/workflows/autograde-service-tests.yml @@ -0,0 +1,23 @@ +name: Spring App tests + +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: set up java + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: build and run the tests for the service + run: mvn -B clean package --file moodle-grading-service/pom.xml + - name: upload junit test report + uses: actions/upload-artifact@v3 + with: + name: test-report + path: moodle-grading-service/target/surefire-reports diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml new file mode 100644 index 0000000000000000000000000000000000000000..85a5b9ac60f4dea00bf2717400833f2f9ab101ce --- /dev/null +++ b/.github/workflows/build-push.yml @@ -0,0 +1,38 @@ +name: Build and Push Docker image for Moodle Grading Service + +# Run this workflow every time a new commit on the main branch changes the moodle-grading-service directory +on: + push: + branches: + - master + paths: + - "moodle-grading-service/**" + - ".github/workflows/build-push.yml" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: ./moodle-grading-service + provenance: false + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/${{ github.repository }}/moodle-grading-service:latest + ghcr.io/${{ github.repository }}/moodle-grading-service:${{ github.sha }} diff --git a/.github/workflows/test-cluster-setup.yml b/.github/workflows/test-cluster-setup.yml new file mode 100644 index 0000000000000000000000000000000000000000..c2563791b49cf759eb8f07a175182a52428eaeff --- /dev/null +++ b/.github/workflows/test-cluster-setup.yml @@ -0,0 +1,62 @@ +name: Deploy to Minikube + +on: + pull_request: + types: [review_requested] + branches: + - master + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Add entry to /etc/hosts + run: | + echo "127.0.0.1 moodle" | sudo tee -a /etc/hosts + + - name: Start minikube + uses: medyagh/setup-minikube@master + id: minikube + with: + driver: docker + mount-path: "${{ github.GITHUB_WORKSPACE }}:/repo" + + - name: Start tunnel + run: | + minikube tunnel & + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build image + run: | + eval $(minikube docker-env) + docker build -t ghcr.io/hamzaremmal/moodle-autograde/moodle-grading-service:latest moodle-grading-service + + - name: Deploy manifests + run: | + kubectl config set-context minikube --namespace=cs107 + kubectl apply -k k8s-overlays/local + + - name: Wait for deployment to complete and grading service to be available + run: | + kubectl rollout status deployment/grading-service + sleep 100 + kubectl get service grading-service-tcp + + - name: Ping grading service + run: | + WEB_SERVICE_URL=$(kubectl get service grading-service-tcp -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo "WEB_SERVICE_URL=$WEB_SERVICE_URL" + response=$(curl -s http://$WEB_SERVICE_URL:8082/api/v1/ping/no-auth) + if [[ "$response" != *"Hello from Spring boot"* ]]; then + echo "Unexpected response: $response" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 82f0c3ac6d4d43fca14022bb41875074ff494d99..5c1cc81f0b25f88591b39a953a5754681eadb244 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -/data/ +#K8S configuration +kubeconfig + +#Local dev variables +**.prod.env diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..13566b81b018ad684f3a35fee301741b2734c8f4 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000000000000000000000000000000000..919ce1f1f77253454105acb2aad9997c1047a0e6 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <ScalaCodeStyleSettings> + <option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" /> + </ScalaCodeStyleSettings> + </code_scheme> +</component> \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000000000000000000000000000000000..a55e7a179bde3e4e772c29c0c85e53354aa54618 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> + </state> +</component> \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000000000000000000000000000000000000..9ac46c72dc290a1ee007f4ceed44861184eeebb5 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CompilerConfiguration"> + <annotationProcessing> + <profile default="true" name="Default" enabled="true" /> + <profile name="Maven default annotation processors profile" enabled="true"> + <sourceOutputDir name="target/generated-sources/annotations" /> + <sourceTestOutputDir name="target/generated-test-sources/test-annotations" /> + <outputRelativeToContentRoot value="true" /> + <module name="moodle-grading-service" /> + </profile> + </annotationProcessing> + </component> + <component name="JavacSettings"> + <option name="ADDITIONAL_OPTIONS_OVERRIDE"> + <module name="moodle-grading-service" options="-parameters" /> + </option> + </component> +</project> \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000000000000000000000000000000000000..039b9aa01e94241da95f10896cf57f7ef2565d3e --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> + <data-source source="LOCAL" name="moodle-dev" uuid="55f0af50-0db1-4bef-8422-991545d60c56"> + <driver-ref>mysql.8</driver-ref> + <synchronize>true</synchronize> + <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver> + <jdbc-url>jdbc:mysql://localhost:3305/moodle</jdbc-url> + <working-dir>$ProjectFileDir$</working-dir> + </data-source> + </component> +</project> \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0903db4abd4624ef0936771c5c849fdfb4e9d64b --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="Encoding"> + <file url="file://$PROJECT_DIR$/moodle-grading-service/src/main/java" charset="UTF-8" /> + <file url="file://$PROJECT_DIR$/moodle-grading-service/src/main/resources" charset="UTF-8" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000000000000000000000000000000000..ffa314ee27bc8a3fb4a73693df7c3399e75abbba --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,15 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true"> + <option name="customHeaders"> + <set> + <option value="API-KEY" /> + </set> + </option> + </inspection_tool> + <inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" /> + <inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" /> + </profile> +</component> \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000000000000000000000000000000000..a522ac4759d00e88fbb1e4e87143289e0ac24fd2 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="RemoteRepositoriesConfiguration"> + <remote-repository> + <option name="id" value="central" /> + <option name="name" value="Maven Central repository" /> + <option name="url" value="https://repo1.maven.org/maven2" /> + </remote-repository> + <remote-repository> + <option name="id" value="jboss.community" /> + <option name="name" value="JBoss Community repository" /> + <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> + </remote-repository> + <remote-repository> + <option name="id" value="central" /> + <option name="name" value="central" /> + <option name="url" value="https://repo1.maven.org/maven2" /> + </remote-repository> + </component> +</project> \ No newline at end of file diff --git a/.idea/jsLinters/jshint.xml b/.idea/jsLinters/jshint.xml new file mode 100644 index 0000000000000000000000000000000000000000..3b0fd0171f2671d282fcedf7e4abf376ff4d2944 --- /dev/null +++ b/.idea/jsLinters/jshint.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JSHintConfiguration" version="2.13.6" use-config-file="true" use-custom-config-file="true" custom-config-file-path="$PROJECT_DIR$/moodle/.jshintrc"> + <option bitwise="true" /> + <option browser="true" /> + <option curly="true" /> + <option eqeqeq="true" /> + <option forin="true" /> + <option maxerr="50" /> + <option noarg="true" /> + <option noempty="true" /> + <option nonew="true" /> + <option strict="true" /> + <option undef="true" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..2acfb33066ba1138f2ea4b1e9f0e2ecb3ff55250 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ExternalStorageConfigurationManager" enabled="true" /> + <component name="MavenProjectsManager"> + <option name="originalFiles"> + <list> + <option value="$PROJECT_DIR$/moodle-grading-service/pom.xml" /> + </list> + </option> + </component> + <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..1c4ebaf4b7cac6d6c29386ae1c9eceaf9a54bf6c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/moodle-autograde.iml" filepath="$PROJECT_DIR$/.idea/moodle-autograde.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/moodle-autograde.iml b/.idea/moodle-autograde.iml new file mode 100644 index 0000000000000000000000000000000000000000..1fb44a343824b2b3a9b726c39b9eb072a41383de --- /dev/null +++ b/.idea/moodle-autograde.iml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/moodle/admin/presets/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/admin/roles/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/admin/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/analytics/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/auth/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/availability/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/backup/controller/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/backup/converter/moodle1/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/backup/moodle2/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/backup/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/backup/util" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/badges/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/blocks/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/blog/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/cache/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/calendar/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/cohort/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/comment/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/competency/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/completion/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/contentbank/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/course/format/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/course/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/customfield/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/enrol/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/favourites/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/files/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/filter/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/grade/grading/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/grade/import/csv/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/grade/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/group/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/h5p/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/iplookup/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/behat/extension" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/ddl/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/dml/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/editor/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/external/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/filebrowser/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/filestorage/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/form/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/grade/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/maxmind/MaxMind/src/MaxMind/Db" isTestSource="false" packagePrefix="MaxMind\Db\" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/maxmind/MaxMind/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/maxmind/MaxMind/tests/MaxMind/Db/Test/Reader" isTestSource="true" packagePrefix="MaxMind\Db\Test\Reader\" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/php-jwt/src" isTestSource="false" packagePrefix="Firebase\JWT\" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/php-jwt/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/phpunit/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/table/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/testing" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/userkey/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/lib/xapi/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/login/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/message/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/mnet/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/my/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/notes/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/payment/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/plagiarism/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/portfolio/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/privacy/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/question/engine/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/question/engine/upgrade/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/question/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/question/type/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/rating/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/reportbuilder/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/repository/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/rss/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/search/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/tag/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/user/tests" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/moodle/webservice/tests" isTestSource="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000000000000000000000000000000000000..0e09af40726c3560d3f3d145e050377be73a1431 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="PhpProjectSharedConfiguration" php_language_level="7.4"> + <option name="suggestChangeDefaultLanguageLevel" value="false" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml new file mode 100644 index 0000000000000000000000000000000000000000..d8758d74baa5fdbd3f0bb65234d731f31cbbcc63 --- /dev/null +++ b/.idea/phpunit.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="PHPUnit"> + <option name="directories"> + <list> + <option value="$PROJECT_DIR$/moodle/lib/php-jwt/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/maxmind/MaxMind/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/phpunit/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/testing" /> + <option value="$PROJECT_DIR$/moodle/lib/ddl/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/dml/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/tests" /> + <option value="$PROJECT_DIR$/moodle/favourites/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/form/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/filestorage/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/filebrowser/tests" /> + <option value="$PROJECT_DIR$/moodle/files/tests" /> + <option value="$PROJECT_DIR$/moodle/filter/tests" /> + <option value="$PROJECT_DIR$/moodle/admin/roles/tests" /> + <option value="$PROJECT_DIR$/moodle/cohort/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/grade/tests" /> + <option value="$PROJECT_DIR$/moodle/grade/tests" /> + <option value="$PROJECT_DIR$/moodle/grade/grading/tests" /> + <option value="$PROJECT_DIR$/moodle/grade/import/csv/tests" /> + <option value="$PROJECT_DIR$/moodle/analytics/tests" /> + <option value="$PROJECT_DIR$/moodle/availability/tests" /> + <option value="$PROJECT_DIR$/moodle/backup/controller/tests" /> + <option value="$PROJECT_DIR$/moodle/backup/converter/moodle1/tests" /> + <option value="$PROJECT_DIR$/moodle/backup/moodle2/tests" /> + <option value="$PROJECT_DIR$/moodle/backup/tests" /> + <option value="$PROJECT_DIR$/moodle/backup/util" /> + <option value="$PROJECT_DIR$/moodle/badges/tests" /> + <option value="$PROJECT_DIR$/moodle/blog/tests" /> + <option value="$PROJECT_DIR$/moodle/customfield/tests" /> + <option value="$PROJECT_DIR$/moodle/iplookup/tests" /> + <option value="$PROJECT_DIR$/moodle/course/tests" /> + <option value="$PROJECT_DIR$/moodle/course/format/tests" /> + <option value="$PROJECT_DIR$/moodle/privacy/tests" /> + <option value="$PROJECT_DIR$/moodle/question/engine/tests" /> + <option value="$PROJECT_DIR$/moodle/question/tests" /> + <option value="$PROJECT_DIR$/moodle/question/type/tests" /> + <option value="$PROJECT_DIR$/moodle/question/engine/upgrade/tests" /> + <option value="$PROJECT_DIR$/moodle/cache/tests" /> + <option value="$PROJECT_DIR$/moodle/calendar/tests" /> + <option value="$PROJECT_DIR$/moodle/enrol/tests" /> + <option value="$PROJECT_DIR$/moodle/group/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/external/tests" /> + <option value="$PROJECT_DIR$/moodle/message/tests" /> + <option value="$PROJECT_DIR$/moodle/notes/tests" /> + <option value="$PROJECT_DIR$/moodle/tag/tests" /> + <option value="$PROJECT_DIR$/moodle/rating/tests" /> + <option value="$PROJECT_DIR$/moodle/repository/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/userkey/tests" /> + <option value="$PROJECT_DIR$/moodle/user/tests" /> + <option value="$PROJECT_DIR$/moodle/webservice/tests" /> + <option value="$PROJECT_DIR$/moodle/mnet/tests" /> + <option value="$PROJECT_DIR$/moodle/completion/tests" /> + <option value="$PROJECT_DIR$/moodle/comment/tests" /> + <option value="$PROJECT_DIR$/moodle/search/tests" /> + <option value="$PROJECT_DIR$/moodle/competency/tests" /> + <option value="$PROJECT_DIR$/moodle/my/tests" /> + <option value="$PROJECT_DIR$/moodle/auth/tests" /> + <option value="$PROJECT_DIR$/moodle/blocks/tests" /> + <option value="$PROJECT_DIR$/moodle/login/tests" /> + <option value="$PROJECT_DIR$/moodle/plagiarism/tests" /> + <option value="$PROJECT_DIR$/moodle/portfolio/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/editor/tests" /> + <option value="$PROJECT_DIR$/moodle/rss/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/table/tests" /> + <option value="$PROJECT_DIR$/moodle/h5p/tests" /> + <option value="$PROJECT_DIR$/moodle/lib/xapi/tests" /> + <option value="$PROJECT_DIR$/moodle/contentbank/tests" /> + <option value="$PROJECT_DIR$/moodle/payment/tests" /> + <option value="$PROJECT_DIR$/moodle/reportbuilder/tests" /> + <option value="$PROJECT_DIR$/moodle/admin/presets/tests" /> + <option value="$PROJECT_DIR$/moodle/admin/tests" /> + </list> + </option> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea42099ae68b53475066a9407ab2461d31ac580b --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + <mapping directory="$PROJECT_DIR$/moodle" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/README.md b/README.md index cd5d17f8db975cc7d2b253a9e0e0eb46d691e7c8..77cf62d1a4d527c6a7f270b7f69813ae62fa3e33 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,84 @@ <div align=center> - <h1>Moodle Dev Environment</h1> + <h1>Moodle Autograde plugin</h1> </div> > ⚠️ The following readme is yet to be completed. For now, it contains basic information about the repository. ## Set up locally -- Install *Docker* and *Docker Compose* +- Install [_minikube_](https://minikube.sigs.k8s.io/docs/start/) and [_kubectl_](https://kubernetes.io/docs/tasks/tools/) on your machine. + - clone the repository using the following command: ```console - git clone --recursive git@github.com:hamzaremmal/moodle-dev-env.git + git clone --recursive git@github.com:hamzaremmal/moodle-autograde.git ``` ## Test your environment -- Start everything with one command : +- Start minikube with the following command: + ```console - docker compose up --build + minikube start --driver=docker --mount --mount-string="$(pwd):/repo" + ``` + + _This will also switch your kubectl context to minikube_ + +- Build the grading service image with the following commands: + + - _Switch to minikube docker environment_ + + ```console + eval $(minikube docker-env) + ``` + + - _Build the image_ + + ```console + docker build -t ghcr.io/hamzaremmal/moodle-autograde/moodle-grading-service:latest moodle-grading-service/ + ``` + +- Configure the dotenv file `k8s-base/grading-service-variables.env` with the following content: + ``` + API_KEY= <Add the autograde service API-KEY> + MOODLE_BASE_URL= <Add the moodle base url, `http://moodle:80` if you are using the local environment> + MOODLE_AUTOGRADE_TOKEN= <Add the autograde moodle token> + GRADING_SERVICE_NAME= <Add the name with which the grading-service is deployed, by default it is `grading-service-tcp`> + ``` + +- Deploy the environment with the following command: + + ```console + kubectl apply -k k8s-overlays/local + ``` + - Kill everything with one command: + ```console - docker compose down + kubectl delete -k k8s-overlays/local ``` -- Access moodle via http://localhost:8080 -- Ping web service with http://localhost:8082/api/v1/ping -> ℹ️ When accessing Moodle for the first time, you will need to install it. This step is pretty simple as all the links with the database are already configured (See config.php ;-) ) +- To access the services, you will need to: + + - Add the following lines to your `/etc/hosts` file: + + ``` + localhost moodle + ``` + - Start a new terminal session and run : + + ```console + minikube tunnel + ``` + + _This will ask for your password because it configures network routing on your machine._ + + - Access Moodle via http://moodle:80 + - Ping the grading service with http://localhost:8082/api/v1/ping + +> ℹ️ When accessing Moodle for the first time, you will need to install it. This step is pretty simple as all the links with the database are already configured (See config.php ;-) ) ## Support + TODO HR : Create an email address in https://groups.epfl.ch diff --git a/config.php b/config.php index 198c7c5c7865474f6785a6bcd4f1db4d8afa8565..1068502106803c12f453b1801d3ac0ca7d44b562 100644 --- a/config.php +++ b/config.php @@ -18,7 +18,7 @@ $CFG->dboptions = array ( 'dbcollation' => 'utf8mb4_0900_ai_ci', ); -$CFG->wwwroot = 'http://localhost:8080'; +$CFG->wwwroot = 'http://moodle:80'; $CFG->dataroot = '/var/www/moodledata'; $CFG->admin = 'admin'; diff --git a/docker-compose.yml b/docker-compose.yml index cac8fff2dbf58a1d62486e507ec9705c026e0e9e..e57335367c3804b720db3d587bff03fd48ad3c32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" services: db: container_name: db @@ -11,18 +11,22 @@ services: MYSQL_PASSWORD: moodle volumes: - db-data:/var/lib/mysql + ports: + - "3305:3306" grading-service: - container_name: grading.service + container_name: grading-service build: context: ./moodle-grading-service dockerfile: Dockerfile ports: - "8082:8082" + env_file: + - grading-service-variables.env moodle: container_name: moodle image: moodlehq/moodle-php-apache:8.0 ports: - - "8080:80" + - "80:80" depends_on: - db environment: @@ -40,7 +44,7 @@ services: - ./php-config:/docker-entrypoint.d - moodle-data:/var/www/moodledata # Mount the plugin - - ./moodle-assignsubmission-autograding:/var/www/html/mod/assign/submission/docker_submission + - ./moodle-assignsubmission-autograde:/var/www/html/mod/assign/submission/autograde selenium: image: "selenium/standalone-firefox" volumes: diff --git a/k8s-base/default-network-policy.yaml b/k8s-base/default-network-policy.yaml new file mode 100644 index 0000000000000000000000000000000000000000..826a627b7b499b34bdee88c38d000c8004cddf57 --- /dev/null +++ b/k8s-base/default-network-policy.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default +spec: + ingress: + - from: + - podSelector: + matchLabels: + network: "default" + podSelector: + matchLabels: + network: "default" diff --git a/k8s-base/grading-service-variables.env b/k8s-base/grading-service-variables.env new file mode 100644 index 0000000000000000000000000000000000000000..fbb87747bde1b98ca8a4ccaf50b1c6881f645179 --- /dev/null +++ b/k8s-base/grading-service-variables.env @@ -0,0 +1,4 @@ +API_KEY="12345" +MOODLE_BASEURL="http://moodle:80" +MOODLE_AUTOGRADE_TOKEN="xxxxxxxxxxxxxx" +GRADING_SERVICE_NAME="grading-service-tcp" diff --git a/k8s-base/grading-service.yaml b/k8s-base/grading-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c16a79cd26845487701f1336b14bf4fdf509558b --- /dev/null +++ b/k8s-base/grading-service.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: grading-service + name: grading-service-tcp +spec: + ports: + - name: "8082" + port: 8082 + targetPort: 8082 + selector: + app: grading-service + type: LoadBalancer +status: + loadBalancer: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grading-service + labels: + app: grading-service +spec: + replicas: 1 + selector: + matchLabels: + app: grading-service + strategy: + type: Recreate + template: + metadata: + labels: + network: default + app: grading-service + spec: + serviceAccountName: moodle + restartPolicy: Always + containers: + - image: ghcr.io/hamzaremmal/moodle-autograde/moodle-grading-service:latest + name: grading-service + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8082 + envFrom: + - secretRef: + name: grading-service-variables + resources: + limits: + memory: "512Mi" + cpu: "500m" +status: {} diff --git a/k8s-base/kustomization.yaml b/k8s-base/kustomization.yaml new file mode 100644 index 0000000000000000000000000000000000000000..82c4278b87ecdda64b970f96a615363e0d25b9e9 --- /dev/null +++ b/k8s-base/kustomization.yaml @@ -0,0 +1,15 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - grading-service.yaml + - default-network-policy.yaml + +secretGenerator: + - name: grading-service-variables + envs: + - grading-service-variables.env + type: Opaque + +generatorOptions: + disableNameSuffixHash: true diff --git a/k8s-overlays/local/db.yaml b/k8s-overlays/local/db.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a7e899612a4c8804e1a1324804990e5459c702c1 --- /dev/null +++ b/k8s-overlays/local/db.yaml @@ -0,0 +1,83 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: db + name: db +spec: + clusterIP: None + ports: + - name: "3305" + port: 3305 + targetPort: 3306 + selector: + app: db + type: ClusterIP +status: + loadBalancer: {} + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: db-data + name: db-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi +status: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: db + name: db +spec: + replicas: 1 + selector: + matchLabels: + app: db + strategy: + type: Recreate + template: + metadata: + labels: + network: "default" + app: db + spec: + containers: + - args: + - --default-authentication-plugin=mysql_native_password + env: + - name: MYSQL_DATABASE + value: moodle + - name: MYSQL_PASSWORD + value: moodle + - name: MYSQL_ROOT_PASSWORD + value: moodle + - name: MYSQL_USER + value: moodle + image: mysql:8.0.33 + name: db + ports: + - containerPort: 3306 + resources: + limits: + cpu: 500m + memory: 500Mi + volumeMounts: + - mountPath: /var/lib/mysql + name: db-data + restartPolicy: Always + volumes: + - name: db-data + persistentVolumeClaim: + claimName: db-data +status: {} diff --git a/k8s-overlays/local/grading-service-account.yaml b/k8s-overlays/local/grading-service-account.yaml new file mode 100644 index 0000000000000000000000000000000000000000..18ca3bf4ff5fff497a4d35e49b365729bcaabfa0 --- /dev/null +++ b/k8s-overlays/local/grading-service-account.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: moodle + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: job-manager-role +rules: + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "update", "delete"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: secret-manager-role +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "update", "delete"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: moodle-job-manager-role-binding +subjects: + - kind: ServiceAccount + name: moodle +roleRef: + kind: Role + name: job-manager-role + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: moodle-secret-manager-role-binding +subjects: + - kind: ServiceAccount + name: moodle +roleRef: + kind: Role + name: secret-manager-role + apiGroup: rbac.authorization.k8s.io diff --git a/k8s-overlays/local/kustomization.yaml b/k8s-overlays/local/kustomization.yaml new file mode 100644 index 0000000000000000000000000000000000000000..26d0a2cb25612370728ef14ef5b9d1aee6db72ad --- /dev/null +++ b/k8s-overlays/local/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: cs107 + +resources: + - ../../k8s-base + - db.yaml + - moodle.yaml + - grading-service-account.yaml + - namespace.yaml diff --git a/k8s-overlays/local/moodle.yaml b/k8s-overlays/local/moodle.yaml new file mode 100644 index 0000000000000000000000000000000000000000..98de3cc93522f72ab22591d3c5f960dcbef0f8b4 --- /dev/null +++ b/k8s-overlays/local/moodle.yaml @@ -0,0 +1,122 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: moodle + name: moodle-tcp +spec: + ports: + - name: "80" + port: 80 + targetPort: 80 + selector: + app: moodle + type: LoadBalancer +status: + loadBalancer: {} + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: moodle + name: moodle +spec: + replicas: 1 + selector: + matchLabels: + app: moodle + strategy: + type: Recreate + template: + metadata: + labels: + network: "default" + app: moodle + spec: + containers: + - env: + - name: MOODLE_DB_HOST + value: db + - name: MOODLE_DB_NAME + value: moodle + - name: MOODLE_DB_PASSWORD + value: moodle + - name: MOODLE_DB_PORT + value: "3306" + - name: MOODLE_DB_TYPE + value: mysqli + - name: MOODLE_DB_USER + value: moodle + - name: MOODLE_URL + value: http://localhost + name: moodle + image: moodlehq/moodle-php-apache:8.0 + ports: + - containerPort: 80 + resources: + limits: + memory: "1000Mi" + cpu: "1000m" + volumeMounts: + - name: moodle-data + mountPath: /var/www/moodledata + - name: moodle-html + mountPath: /var/www/html + - name: moodle-config + mountPath: /var/www/html/config.php + - name: php-config + mountPath: /docker-entrypoint.d + - name: moodle-autograde + mountPath: /var/www/html/mod/assign/submission/autograde + restartPolicy: Always + volumes: + - name: moodle-data + persistentVolumeClaim: + claimName: moodle-data-pvc + - name: moodle-html + hostPath: + path: /repo/moodle + - name: moodle-config + hostPath: + path: /repo/config.php + - name: php-config + hostPath: + path: /repo/php-config + - name: moodle-autograde + hostPath: + path: /repo/moodle-assignsubmission-autograde +status: {} + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: moodle-data + name: moodle-data-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +status: {} + +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + labels: + app: moodle-data + name: moodle-data-pv +spec: + storageClassName: manual + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + hostPath: + path: /data/moodle-data/ diff --git a/k8s-overlays/local/namespace.yaml b/k8s-overlays/local/namespace.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d0dcc911e9e4f9ddd38539c52d3f8830a25489c0 --- /dev/null +++ b/k8s-overlays/local/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cs107 diff --git a/k8s-overlays/prod/increase_memory.yaml b/k8s-overlays/prod/increase_memory.yaml new file mode 100644 index 0000000000000000000000000000000000000000..10621592f857707c1e4025ae55856ed1a1052ae8 --- /dev/null +++ b/k8s-overlays/prod/increase_memory.yaml @@ -0,0 +1,16 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grading-service +spec: + selector: + matchLabels: + app: grading-service + template: + spec: + containers: + - name: grading-service + resources: + limits: + memory: "2Gi" + cpu: "1000m" diff --git a/k8s-overlays/prod/kustomization.yaml b/k8s-overlays/prod/kustomization.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f465e2d1c5e90abbc2fd0ecc8db9ce2fc5e47880 --- /dev/null +++ b/k8s-overlays/prod/kustomization.yaml @@ -0,0 +1,15 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../k8s-base + +patchesStrategicMerge: + - increase_memory.yaml + +secretGenerator: + - name: grading-service-variables + envs: + - grading-service-variables.prod.env + type: Opaque + behavior: replace diff --git a/moodle-assignsubmission-autograde/.gitignore b/moodle-assignsubmission-autograde/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cc260178a15d57c37af1b42e18f818e54064cdd4 --- /dev/null +++ b/moodle-assignsubmission-autograde/.gitignore @@ -0,0 +1,79 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + diff --git a/moodle-assignsubmission-autograde/.idea/dataSources.xml b/moodle-assignsubmission-autograde/.idea/dataSources.xml new file mode 100644 index 0000000000000000000000000000000000000000..379a325f05d9fe0867a6929a3ff2f5d6b7d00725 --- /dev/null +++ b/moodle-assignsubmission-autograde/.idea/dataSources.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> + <data-source source="LOCAL" name="moodle" uuid="6470205f-982c-4b3e-91bb-6aed7cd4bd20"> + <driver-ref>mysql.8</driver-ref> + <synchronize>true</synchronize> + <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver> + <jdbc-url>jdbc:mysql://localhost:3305/moodle</jdbc-url> + <working-dir>$ProjectFileDir$</working-dir> + </data-source> + </component> +</project> \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/.idea/modules.xml b/moodle-assignsubmission-autograde/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..c81fe137a1fc903edb7ea7636092bb7585ea978c --- /dev/null +++ b/moodle-assignsubmission-autograde/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/moodle-assignsubmission-autograde.iml" filepath="$PROJECT_DIR$/.idea/moodle-assignsubmission-autograde.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/.idea/moodle-assignsubmission-autograde.iml b/moodle-assignsubmission-autograde/.idea/moodle-assignsubmission-autograde.iml new file mode 100644 index 0000000000000000000000000000000000000000..239769462a25502a125be21849b063f9ff9c23d6 --- /dev/null +++ b/moodle-assignsubmission-autograde/.idea/moodle-assignsubmission-autograde.iml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="WEB_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/classes" isTestSource="false" packagePrefix="assignsubmission_autograde" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/.idea/php.xml b/moodle-assignsubmission-autograde/.idea/php.xml new file mode 100644 index 0000000000000000000000000000000000000000..5fe959c3187d05713710627214df6262ddfd8d37 --- /dev/null +++ b/moodle-assignsubmission-autograde/.idea/php.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="MessDetectorOptionsConfiguration"> + <option name="transferred" value="true" /> + </component> + <component name="PHPCSFixerOptionsConfiguration"> + <option name="transferred" value="true" /> + </component> + <component name="PHPCodeSnifferOptionsConfiguration"> + <option name="highlightLevel" value="WARNING" /> + <option name="transferred" value="true" /> + </component> + <component name="PhpIncludePathManager"> + <include_path> + <path value="$PROJECT_DIR$/../moodle" /> + </include_path> + </component> + <component name="PhpProjectSharedConfiguration" php_language_level="7.1"> + <option name="suggestChangeDefaultLanguageLevel" value="false" /> + </component> + <component name="PhpStanOptionsConfiguration"> + <option name="transferred" value="true" /> + </component> + <component name="PsalmOptionsConfiguration"> + <option name="transferred" value="true" /> + </component> +</project> \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/.idea/vcs.xml b/moodle-assignsubmission-autograde/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c0b8635858dc7ad44b93df54b762707ce49eefc --- /dev/null +++ b/moodle-assignsubmission-autograde/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$/.." vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/moodle-assignsubmission-autograding/README.md b/moodle-assignsubmission-autograde/README.md similarity index 100% rename from moodle-assignsubmission-autograding/README.md rename to moodle-assignsubmission-autograde/README.md diff --git a/moodle-assignsubmission-autograde/classes/autograde_webservice.php b/moodle-assignsubmission-autograde/classes/autograde_webservice.php new file mode 100644 index 0000000000000000000000000000000000000000..cc7b905848160faf56c2d8281fceaad19178eef8 --- /dev/null +++ b/moodle-assignsubmission-autograde/classes/autograde_webservice.php @@ -0,0 +1,167 @@ +<?php + +namespace assignsubmission_autograde; + +use dml_exception; +use stdClass; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +final class autograde_webservice { + + private string $api_key; + + /** + * @param string $api_key ??? + */ + public function __construct(string $api_key) { + $this->api_key = $api_key; + } + + /** + * ??? + * @param int $timeout ??? + * @param bool $auth ??? + * @return mixed ??? + * @throws dml_exception ??? + */ + public function ping(int $timeout = 2, bool $auth = false) { + // HR : Build the URL fot the grade function + $url = $auth + ? $this->url('/api/v1/ping/auth') + : $this->url('/api/v1/ping/no-auth'); + // HR : Prepare the headers + $headers = [ + $this->api_key_header($this->api_key) + ]; + // HR : Send request using cURL + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); + // HR : Only add the key in auth mode + if ($auth) + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_exec($curl); + curl_close($curl); + // Return the response + return curl_getinfo($curl); + } + + /** + * ??? + * @param $userid ??? + * @param $courseid ??? + * @param $assignmentid ??? + * @param $submissionid ??? + * @return stdClass ??? + * @throws dml_exception ??? + */ + public function grade( + $userid, $courseid, + $assignmentid, $submissionid, + $graderImageName, $dockerRegistryCredential + ) : stdClass { + // HR : Build the URL for the grade function + $url = $this->url('/api/v1/grading/grade'); + // HR : Prepare the request body (JSON) + $body = array( + 'userid' => $userid, + 'courseid' => $courseid, + 'assignmentid' => $assignmentid, + 'submissionid' => $submissionid, + 'imageName' => $graderImageName, + 'registryCredential' => $dockerRegistryCredential + ); + // HR : Prepare the headers + $headers = [ + $this->api_key_header($this->api_key), + 'Content-Type: application/json' + ]; + // HR : Prepare cURL options + $options = array( + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers + ); + // HR : Send request using cURL + $curl = curl_init(); + curl_setopt_array($curl, $options); + $response = curl_exec($curl); + curl_close($curl); + // Return the response + $r = new stdClass(); + $r->response = $response; + $r->info = curl_getinfo($curl); + return $r; + } + + /** + * ??? + * @param $courseid ??? + * @param $assignmentid + * @return stdClass ??? + * @throws dml_exception + */ + public function upload_credentials($courseid, $assignmentid): stdClass { + // HR : Build the URL for the grade function + $url = $this->url('/api/v1/registry/credentials/upload'); + // HR : Prepare the request body (JSON) + $body = array( + 'courseid' => $courseid, + 'assignmentid' => $assignmentid, + ); + // HR : Prepare the headers + $headers = [ + $this->api_key_header($this->api_key), + 'Content-Type: application/json' + ]; + // HR : Prepare cURL options + $options = array( + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers + ); + // HR : Send request using cURL + $curl = curl_init(); + curl_setopt_array($curl, $options); + $response = curl_exec($curl); + curl_close($curl); + // Return the response + $r = new stdClass(); + $r->response = $response; + $r->info = curl_getinfo($curl); + return $r; + } + + // ============================================================================================ + // =================================== HELPER METHODS ========================================= + // ============================================================================================ + + /** + * ??? + * @param string $endpoint ??? + * @return string ??? + * @throws dml_exception ??? + */ + private function url(string $endpoint): string { + return get_config('assignsubmission_autograde', 'service_url') . $endpoint; + } + + /** + * ??? + * @param string $key ??? + * @return string ??? + */ + private function api_key_header(string $key) : string { + return 'API-KEY: ' . $key; + } + +} \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php b/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php new file mode 100644 index 0000000000000000000000000000000000000000..ccffa273957db8f0221dc7dcdf2f8be78a14a6cd --- /dev/null +++ b/moodle-assignsubmission-autograde/classes/external/autograde_download_credentials.php @@ -0,0 +1,90 @@ +<?php + +namespace assignsubmission_autograde\external; + +use coding_exception; +use context_course; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +final class autograde_download_credentials extends external_api { + + /** + * ??? + * @return external_function_parameters ??? + */ + 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') + ]); + } + + /** + * ??? + * @param $courseid ??? + * @param $assignmentid ??? + * @return object ??? + * @throws coding_exception ??? + * @throws moodle_exception ??? + */ + public static function download_credentials($courseid, $assignmentid): object { + $context = context_course::instance($courseid); + $component = 'assignsubmission_autograde'; + $filearea = 'credentials'; + $fs = get_file_storage(); + $files = $fs->get_area_files( + $context->id, + $component, + $filearea, + 0, + 'itemid, filepath, filename', + false + ); + // HR : Check if files are found + if (!empty($files)) { + // HR : Iterate over al the available files + foreach ($files as $file){ + // HR : If we find a non-empty file, return it + // HR : We allow only one file for now + if(!empty($file->get_content())){ + // HR : Return the base64 encoding of the file's content + return (object) [ + 'content' => base64_encode($file->get_content()) + ]; + } + } + } else { + // No files found, return an error + throw new moodle_exception('nofilefound', 'autograde'); + } + throw new moodle_exception('all files are empty', 'autograde'); + } + + /** + * ??? + * @return external_single_structure ??? + */ + public static function download_credentials_returns(): external_single_structure{ + return new external_single_structure([ + 'content' => new external_value(PARAM_TEXT, 'File Content') + ]); + } + +} \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php b/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php new file mode 100644 index 0000000000000000000000000000000000000000..638b13e68f50fdd6b1c1f856eadc4f84c58acb73 --- /dev/null +++ b/moodle-assignsubmission-autograde/classes/external/autograde_download_submission.php @@ -0,0 +1,92 @@ +<?php + +namespace assignsubmission_autograde\external; + + +use coding_exception; +use context_course; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use moodle_exception; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +final class autograde_download_submission extends external_api { + + /** + * ??? + * @return external_function_parameters ??? + */ + 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') + ]); + } + + /** + * ??? + * @param $courseid ??? + * @param $submissionid ??? + * @return stdClass ??? + * @throws coding_exception ??? + * @throws moodle_exception ??? + */ + public static function download_submission($courseid, $submissionid): stdClass { + $context = context_course::instance($courseid); + $component = 'assignsubmission_autograde'; + $filearea = 'submissions'; + // HR : Retrieve the files associated with the specified area and item + $fs = get_file_storage(); + $files = $fs->get_area_files( + $context->id, + $component, + $filearea, + $submissionid, + 'itemid, filepath, filename', + false + ); + // HR : Check if files are found + if (!empty($files)) { + // HR : Iterate over al the available files + foreach ($files as $file){ + // HR : If we find a non-empty file, return it + // HR : We allow only one file for now + if(!empty($file->get_content())){ + // HR : Return the base64 encoding of the file's content + return (object) [ + 'content' => base64_encode($file->get_content()) + ]; + } + } + } else { + // No files found, return an error + throw new moodle_exception('nofilefound', 'autograde'); + } + throw new moodle_exception('all files are empty', 'autograde'); + } + + /** + * ??? + * @return external_single_structure ??? + */ + public static function download_submission_returns(): external_single_structure{ + return new external_single_structure([ + 'content' => new external_value(PARAM_TEXT, 'File Content') + ]); + } + +} \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php b/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php new file mode 100644 index 0000000000000000000000000000000000000000..8b435bff53b2162777db1a8cff7014f26da2cfcb --- /dev/null +++ b/moodle-assignsubmission-autograde/classes/external/autograde_upload_feedback.php @@ -0,0 +1,133 @@ +<?php + +namespace assignsubmission_autograde\external; + +use external_api; +use external_function_parameters; +use external_value; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/mod/assign/locallib.php'); + + +/** + * ??? + * + * See : https://moodledev.io/docs/apis/subsystems/external/functions + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +final class autograde_upload_feedback extends external_api { + + /** + * ??? + * @return external_function_parameters ??? + */ + 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'), + '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'), + ]); + } + + /** + * ??? + * @param $userid ??? + * @param $courseid ??? + * @param $assignmentid ??? + * @param $submissionid ??? + * @param $grade ??? + * @param $feedback ??? + * @return true ??? + */ + public static function upload_feedback ( + $userid, $courseid, + $assignmentid, $submissionid, + $grade, $feedback + ) { + + global $DB, $USER; + + // HR : Fetch metadata about the assignment + $assignment = $DB->get_record('assign', array('id' => $assignmentid)); + + // HR : Sanity check : Are the actual course id similar to the expected one ? + //if ($assignment->course != $courseid) { + // throw new coding_exception('Invalid assignment or course ID. Please verify the assignment ('.$assignmentid.') and course details.'); + //} + + // HR : Fetch the metadata of the submission + $submission = $DB->get_record('assign_submission', array('id' => $submissionid)); + + // HR : Sanity check : Is the submission part of the course and is it linked to the user ? + //if ($submission->assignment != $assignmentid || $submission->userid != $userid) { + // throw new coding_exception('Invalid submission or user ID'); + //} + + // ================================ assign_grades table =================================== + + // HR : Create an entry in assign_grades + $gradegrade = new stdClass(); + $gradegrade->assignment = $assignment->id; // HR : Link the entry to the assignment + $gradegrade->userid = $submission->userid; // HR : Link the entry to the user + $gradegrade->grade = $grade; // HR : Store the actual grade + $gradegrade->feedback = $feedback; // HR : Store some comment as a feedback + $gradegrade->grader = $USER->id; // HR : Identify the grader (inferred from the context) + $gradegrade->timecreated = time(); // HR : Timestamp + $gradegrade->timemodified = time(); // HR : Timestamp + + // HR : Save the above entry in the assign_grades table + $gradegrade->id = $DB->insert_record('assign_grades', $gradegrade); + + // ==================================== grade_grades table ================================ + + $gradeitem = $DB->get_record('grade_items', ['itemmodule' => 'assign', 'iteminstance' => $assignmentid]); + + $gradegradedata = new stdClass(); + $gradegradedata->itemid = $gradeitem->id; + $gradegradedata->userid = $submission->userid; + $gradegradedata->rawgrade = $grade; + $gradegradedata->finalgrade = $grade; + $gradegradedata->usermodified = $userid; + $gradegradedata->hidden = 0; + $gradegradedata->timecreated = time(); + $gradegradedata->timemodified = time(); + $gradegradedata->aggregationstatus = 'used'; + $gradegradedata->aggregationweight = 1; + + // HR : Save the grade in the GradeBook + $gradegradedata->id = $DB->insert_record('grade_grades', $gradegradedata); + + // ============================== feedback comment plugin support ========================== + + //$feedbackcomment = new stdClass(); + //$feedbackcomment->commenttext = $feedback; + //$feedbackcomment->commentformat = FORMAT_HTML; + //$feedbackcomment->grade = $gradegrade->id; + //$feedbackcomment->assignment = $assignmentid; + //$DB->insert_record('assignfeedback_comments', $feedbackcomment); + + //$grade_item = grade_item::fetch(array('id' => $assignmentid)); + //$grade_item->force_regrading(); + //$grade_item->update_final_grade($userid, $newgrade); + //$grade_item->regrade_final_grades($userid); + + return true; + } + + /** + * ??? + * @return external_value ??? + */ + public static function upload_feedback_returns() { + return new external_value(PARAM_BOOL, 'Feedback added successfully'); + } +} diff --git a/moodle-assignsubmission-autograde/css/autograde.css b/moodle-assignsubmission-autograde/css/autograde.css new file mode 100644 index 0000000000000000000000000000000000000000..bc5039b00d44fbed778c5f78212793fb97aa072d --- /dev/null +++ b/moodle-assignsubmission-autograde/css/autograde.css @@ -0,0 +1,3 @@ +h2 { + color: aqua; +} \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/db/services.php b/moodle-assignsubmission-autograde/db/services.php new file mode 100644 index 0000000000000000000000000000000000000000..5b5181e592aa0ee122772544b0255359d119c051 --- /dev/null +++ b/moodle-assignsubmission-autograde/db/services.php @@ -0,0 +1,42 @@ +<?php + +defined('MOODLE_INTERNAL') || die(); + + +// https://moodledev.io/docs/apis/subsystems/external/description + +$functions = [ + 'mod_assignsubmission_autograde_upload_feedback' => [ + 'classname' => 'assignsubmission_autograde\external\autograde_upload_feedback', + 'methodname' => 'upload_feedback', + 'description' => 'Upload an autograde feedback', + //'capabilities' => '', + ], + 'mod_assignsubmission_autograde_download_submission' => [ + 'classname' => 'assignsubmission_autograde\external\autograde_download_submission', + 'methodname' => 'download_submission', + 'description' => 'Download an autograde submission', + //'capabilities' => '', + ], + 'mod_assignsubmission_autograde_download_credentials' => [ + 'classname' => 'assignsubmission_autograde\external\autograde_download_credentials', + 'methodname' => 'download_credentials', + 'description' => 'Download an autograde credential', + //'capabilities' => '', + ], +]; + +// https://moodledev.io/docs/apis/subsystems/external/advanced/custom-services + +$services = [ + 'AUTOGRADE' => [ + 'functions' => [ + 'mod_assignsubmission_autograde_upload_feedback', + 'mod_assignsubmission_autograde_download_submission', + 'mod_assignsubmission_autograde_download_credentials' + ], + 'restrictedusers' => 1, + 'enabled' => 1, + 'shortname' => 'autograde-service' + ] +]; diff --git a/moodle-assignsubmission-autograding/lang/en/assignsubmission_docker_submission.php b/moodle-assignsubmission-autograde/lang/en/assignsubmission_autograde.php similarity index 69% rename from moodle-assignsubmission-autograding/lang/en/assignsubmission_docker_submission.php rename to moodle-assignsubmission-autograde/lang/en/assignsubmission_autograde.php index b22eb6bf2a0d715000e0e7c56007ec5b23eb3509..36b999f5763dc76627df328fe0bab8c1cf5adaf3 100644 --- a/moodle-assignsubmission-autograding/lang/en/assignsubmission_docker_submission.php +++ b/moodle-assignsubmission-autograde/lang/en/assignsubmission_autograde.php @@ -1,6 +1,6 @@ <?php -$string["pluginname"] = "Docker Submission"; +$string["pluginname"] = "Autograde"; // ====================================== FORM ELEMENTS USER ====================================== @@ -14,4 +14,10 @@ $string["student_view_summary_header"] = "Feedback"; $string["setting_docker-image"] = "Docker Image"; $string["setting_docker-image_help"] = "Help Docker Image"; +$string["setting_docker-registry-token"] = "Docker Registry Token"; +$string["setting_docker-registry-token_help"] = "Help Docker Registry Setting"; + +$string["setting_webservice-key"] = "Web Service Key"; +$string["setting_webservice-key_help"] = "Help Web Service Key Setting"; + // ======================================= ADMIN SETTINGS ========================================= \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/lib.php b/moodle-assignsubmission-autograde/lib.php new file mode 100644 index 0000000000000000000000000000000000000000..6cf2904440545be201da82658728b9a9fd6e7b4b --- /dev/null +++ b/moodle-assignsubmission-autograde/lib.php @@ -0,0 +1,3 @@ +<?php + +// TODO HR : Add all the generic functions implemented by this plugin \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/locallib.php b/moodle-assignsubmission-autograde/locallib.php new file mode 100644 index 0000000000000000000000000000000000000000..29d41f201ac6bc7d04ccf2d77c12f12a9626c06b --- /dev/null +++ b/moodle-assignsubmission-autograde/locallib.php @@ -0,0 +1,391 @@ +<?php + +use assignsubmission_autograde\autograde_webservice; +use core\notification; + +defined('MOODLE_INTERNAL') || die(); + +const ASSIGNSUBMISSION_AUTOGRADE_SUBMISSION_FILEAREA = 'submissions'; + +const ASSIGNSUBMISSION_AUTOGRADE_CREDENTIALS_FILEAREA = 'credentials'; + +/** + * Autograde Submission plugin. + * + * This plugin serves submissions to a webserver, so they can be graded automatically. + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +final class assign_submission_autograde extends assign_submission_plugin { + + private const COMPONENT_NAME = "assignsubmission_autograde"; + private const setting_docker_image_id = "docker_image"; + + private const setting_docker_registry_token_id = "docker_registry_token"; + + private const setting_webservice_key_id = "webservice_key"; + + + /** + * Get the name of this plugin + * + * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#get_name + * @return string plugin name + * @throws coding_exception + */ + public function get_name(): string { + return get_string("pluginname", self::COMPONENT_NAME); + } + + // ============================================================================================ + // ======================================= SETTINGS =========================================== + // ============================================================================================ + + /** + * Configure the settings for the docker_submission plugin + * + * We only need the docker image of the grader to run + * @note See documentation : https://docs.moodle.org/dev/Form_API + * @param MoodleQuickForm $mform + * @return void + * @throws coding_exception + */ + public function get_settings(MoodleQuickForm $mform) { + // HR : More details : https://docs.moodle.org/dev/lib/formslib.php_Form_Definition#text + $mform->addElement("text", + self::setting_docker_image_id, + get_string("setting_docker-image", self::COMPONENT_NAME), + null); + // HR : More details : https://docs.moodle.org/dev/lib/formslib.php_Form_Definition#addHelpButton + $mform->addHelpButton( + self::setting_docker_image_id, + "setting_docker-image", + self::COMPONENT_NAME); + // HR : See : https://docs.moodle.org/dev/lib/formslib.php_Form_Definition#hideIf + $mform->hideIf( + self::setting_docker_image_id, + 'assignsubmission_autograde_enabled', + 'notchecked'); + + // HR : Add filemanager to store the docker registry token + $credentials_options = $this->get_credentials_option(); + $mform->addElement("filemanager", "credentials", "Registry Credentials", null, $credentials_options); + $mform->hideIf( + 'credentials', + 'assignsubmission_autograde_enabled', + 'notchecked'); + + // HR : Add entry to store the API_KEY + $mform->addElement( + "password", + self::setting_webservice_key_id, + get_string("setting_webservice-key", self::COMPONENT_NAME), + null); + $mform->addHelpButton( + self::setting_webservice_key_id, + "setting_webservice-key", + self::COMPONENT_NAME); + $mform->hideIf( + self::setting_webservice_key_id, + 'assignsubmission_autograde_enabled', + 'notchecked'); + } + + /** + * Save the settings of the docker_submission plugin + * + * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#save_settings + * @param stdClass $data ??? + * @return bool ??? + * @throws coding_exception + * @throws dml_exception + */ + public function save_settings(stdClass $data): bool { + // HR : Store the Docker image in the configuration + global $COURSE; + $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); + + // HR : Save the credentials in the corresponding file area + file_save_draft_area_files( + $data->credentials, + context_course::instance($COURSE->id)->id, + self::COMPONENT_NAME, + ASSIGNSUBMISSION_AUTOGRADE_CREDENTIALS_FILEAREA, + 0 + ); + + // HR : Send request to the autograde service to update the credentials + // HR : also test the API_KEY + $autograde_webservice = new autograde_webservice($this->get_config(self::setting_webservice_key_id)); + $response = $autograde_webservice->upload_credentials($COURSE->id, $this->assignment->get_instance()->id); + // HR : Analyze the response + if ($response->info['http_code'] == 200){ + notification::info('The credentials were correctly updated in the service'); + } else if ($response->info['http_code'] == 403) { + $this->set_error('The provided API_KEY is not valid !'); + return false; + } else if($response->info['http_code'] == 500){ + $this->set_error("internal error : " . $response->response); + return false; + } else if($response->info['http_code'] == 0){ + $this->set_error("autograde service not found. please contact the support."); + return false; + } + return true; + } + + /** + * ??? + * @param $defaultvalues ??? + * @return void ??? + */ + public function data_preprocessing(&$defaultvalues) { + // HR : Call the parent function + global $COURSE; + parent::data_preprocessing($defaultvalues); + // HR : Fill in the docker image setting if it was previously filled + $defaultvalues[self::setting_docker_image_id] = + $this->get_config(self::setting_docker_image_id); + // HR : Fill in the API-KEY setting if it was previously filled + $defaultvalues[self::setting_webservice_key_id] = + $this->get_config(self::setting_webservice_key_id); + + $draftitemid = file_get_submitted_draft_itemid('credentials'); + file_prepare_draft_area( + $draftitemid, + context_course::instance($COURSE->id)->id, + self::COMPONENT_NAME, + ASSIGNSUBMISSION_AUTOGRADE_CREDENTIALS_FILEAREA, + 0, + $this->get_credentials_option() + ); + $defaultvalues['credentials'] = $draftitemid; + } + + // ============================================================================================ + // ================================== SUBMISSION FORM ========================================= + // ============================================================================================ + + /** + * ??? + * + * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#get_form_elements + * @param $submissionorgrade + * @param MoodleQuickForm $mform + * @param stdClass $data + * @param $userid + * @return bool + * @throws coding_exception + */ + public function get_form_elements_for_user($submissionorgrade, + MoodleQuickForm $mform, + stdClass $data, + $userid): bool { + global $COURSE; + // HR : Accepted files + $fileoptions = $this->get_submission_options(); + + // HR : Prepare the filemanager for the submission + $data = file_prepare_standard_filemanager( + $data, + 'tasks', + $fileoptions, + context_course::instance($COURSE->id), + self::COMPONENT_NAME, + ASSIGNSUBMISSION_AUTOGRADE_SUBMISSION_FILEAREA, + $submissionorgrade->id); + + $name = get_string("form_filemanager", self::COMPONENT_NAME); + $element_id = "tasks_filemanager"; + $mform->addElement('filemanager', $element_id, $name, null, $fileoptions); + + $mform->addHelpButton( + $element_id, + "form_filemanager", + self::COMPONENT_NAME); + + return true; + } + + // ============================================================================================ + // ================================== INTERACTION WITH USER =================================== + // ============================================================================================ + + + /** + * ??? + * @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 + $data = file_postupdate_standard_filemanager($data, + 'tasks', + $this->get_submission_options(), + context_course::instance($COURSE->id), + self::COMPONENT_NAME, + ASSIGNSUBMISSION_AUTOGRADE_SUBMISSION_FILEAREA, + $submissionorgrade->id); + + // HR : Build the webservice API + $autograde_webservice = new autograde_webservice($this->get_config(self::setting_webservice_key_id)); + // HR : Request from autograde to grade the submission + $response = $autograde_webservice->grade( + $USER->id, + $COURSE->id, + $this->assignment->get_instance()->id, + $submissionorgrade->id, + $this->get_config(self::setting_docker_image_id), + $this->get_config(self::setting_docker_registry_token_id) + ); + // HR : Process the autograde webservice response + if($response->info === false){ + $this->set_error("Request failed (Server not responding)!"); + return false; + } else if($response->info['http_code'] == 200){ + notification::success("You submission was scheduled for grading"); + } else if($response->info['http_code'] == 403){ + $this->set_error('You submission was not scheduled for grading. Please contact your teacher or one of the TAs to fix the issue.'); + return false; + } else { + $this->set_error('code : ' . $response->info['http_code'] . ' | body : ' . $response->response); + return false; + } + return true; + } + + public function delete_instance(): bool { + // TODO HR : Implement this function + return false; + } + + // ============================================================================================ + // ==================================== FILE MANAGEMENT ======================================= + // ============================================================================================ + + /** + * ??? + * + * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#get_files + * @param stdClass $submissionorgrade + * @param stdClass $user + * @return array + * @throws coding_exception + */ + public function get_files(stdClass $submissionorgrade, stdClass $user): array { + $result = []; + $fs = get_file_storage(); + + $files = $fs->get_area_files( + $this->assignment->get_context()->id, + 'assignsubmission_file', + ASSIGNSUBMISSION_AUTOGRADE_SUBMISSION_FILEAREA, + $submissionorgrade->id, + 'timemodified', + false + ); + + foreach ($files as $file) { + $result[$file->get_filename()] = $file; + } + return $result; + } + + /** + * ??? + * @return string[] ??? + */ + public function get_file_areas(): array { + return array( + ASSIGNSUBMISSION_AUTOGRADE_SUBMISSION_FILEAREA => "store the submissions", + ASSIGNSUBMISSION_AUTOGRADE_CREDENTIALS_FILEAREA => "store the credentials" + ); + } + + // ============================================================================================ + // ========================================= VIEWS ============================================ + // ============================================================================================ + + /** + * View of the summary (visible by the student) + * + * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#view_summary + * @param $submission ??? + * @param bool &$showviewlink ??? + * @return string ??? + */ + public function view_summary($submission, &$showviewlink): string { + // TODO HR : 1- Fetch the path to the file to render from the database + // TODO HR : 2- Render the file in the view + // TODO HR : Should also handle the case where the submission is being graded + return " + <h2 align=center>Feedback for $submission->id</h2> + <details> + <summary>Test #1</summary> + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque tincidunt ligula est. Suspendisse ac aliquam est. Etiam venenatis, ante a. + </p> + </details> + <details> + <summary>Test #2</summary> + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque tincidunt ligula est. Suspendisse ac aliquam est. Etiam venenatis, ante a. + </p> + </details> + "; + } + + // ============================================================================================ + // ====================================== OTHERS ? ============================================ + // ============================================================================================ + + /** + * ??? + * + * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#is_empty + * @param stdClass $submissionorgrade + * @return bool + * @throws coding_exception + */ + public function is_empty(stdClass $submissionorgrade): bool { + // HR : Check if there is a file in the SUBMISSION_FILEAREA + global $COURSE; + $fs = get_file_storage(); + $files = $fs->get_area_files( + context_course::instance($COURSE->id)->id, + self::COMPONENT_NAME, + ASSIGNSUBMISSION_AUTOGRADE_SUBMISSION_FILEAREA, + $submissionorgrade->id, + 'id'); + // HR : The submission is considered empty if no file is stored in the file_area + return count($files) == 0; + } + + /** + * ??? + * + * @return array ??? + */ + private function get_submission_options(): array{ + return array( + 'subdirs' => 1, + "maxfiles" => 1, + 'accepted_types' => array(".zip"), + 'return_types' => FILE_INTERNAL + ); + } + + private function get_credentials_option() : array { + return array( + 'subdirs' => 1, + "maxfiles" => 1, + 'accepted_types' => array(".config"), + 'return_types' => FILE_INTERNAL | FILE_EXTERNAL + ); + } + +} \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/settings.php b/moodle-assignsubmission-autograde/settings.php new file mode 100644 index 0000000000000000000000000000000000000000..a815f99fd1a80db90b56d30cf17bf44f954c4824 --- /dev/null +++ b/moodle-assignsubmission-autograde/settings.php @@ -0,0 +1,10 @@ +<?php + +defined('MOODLE_INTERNAL') || die(); + +$settings->add( + new admin_setting_configtext("assignsubmission_autograde/service_url", + "Service URL", + "URL to the autograde service", + "") +); \ No newline at end of file diff --git a/moodle-assignsubmission-autograde/version.php b/moodle-assignsubmission-autograde/version.php new file mode 100644 index 0000000000000000000000000000000000000000..24d9bcc29d3fb99b8a110f65e179df5b6b2699d9 --- /dev/null +++ b/moodle-assignsubmission-autograde/version.php @@ -0,0 +1,21 @@ +<?php + +defined('MOODLE_INTERNAL') || die(); + +/** ??? */ +$plugin->version = 2023042402; +/** ??? */ +$plugin->requires = 2022111800; +/** ???? */ +$plugin->component = 'assignsubmission_autograde'; +/** ??? */ +$plugin->dependencies = array( + 'mod_assign' => ANY_VERSION, +); +/** ??? */ +$plugin->stylesheets = array( + 'autograde' => array( + 'file' => 'css/autograde.css', + 'media' => 'all', + ), +); diff --git a/moodle-assignsubmission-autograding/locallib.php b/moodle-assignsubmission-autograding/locallib.php deleted file mode 100644 index 81e46a8566b685a7b0d1f6a23bd14e8799ccbe77..0000000000000000000000000000000000000000 --- a/moodle-assignsubmission-autograding/locallib.php +++ /dev/null @@ -1,296 +0,0 @@ -<?php - -defined('MOODLE_INTERNAL') || die(); - -const ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA = 'submissions'; - -/** - * ??? - */ -final class assign_submission_docker_submission extends assign_submission_plugin { - - private const COMPONENT_NAME = "assignsubmission_docker_submission"; - private const setting_docker_image_id = "docker_image"; - - /** - * Get the name of this plugin - * - * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#get_name - * @return string plugin name - * @throws coding_exception - */ - public function get_name(): string { - return get_string("pluginname", self::COMPONENT_NAME); - } - - // ============================================================================================ - // ======================================= SETTINGS =========================================== - // ============================================================================================ - - /** - * Configure the settings for the docker_submission plugin - * - * We only need the docker image of the grader to run - * @note See documentation : https://docs.moodle.org/dev/Form_API - * @param MoodleQuickForm $mform - * @return void - * @throws coding_exception - */ - public function get_settings(MoodleQuickForm $mform) { - $name = get_string("setting_docker-image", self::COMPONENT_NAME); - // HR : More details : https://docs.moodle.org/dev/lib/formslib.php_Form_Definition#text - $mform->addElement("text", self::setting_docker_image_id, $name, null); - // HR : More details : https://docs.moodle.org/dev/lib/formslib.php_Form_Definition#addHelpButton - $mform->addHelpButton(self::setting_docker_image_id, "setting_docker-image", self::COMPONENT_NAME); - // HR : See : https://docs.moodle.org/dev/lib/formslib.php_Form_Definition#hideIf - $mform->hideIf(self::setting_docker_image_id, - 'assignsubmission_docker_submission_enabled', - 'notchecked'); - } - - /** - * Save the settings of the docker_submission plugin - * - * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#save_settings - * @param stdClass $data - * @return bool - */ - public function save_settings(stdClass $data): bool { - $this->set_config(self::setting_docker_image_id, $data->docker_image); - return true; - } - - public function data_preprocessing(&$defaultvalues) { - // TODO HR : Implement this function - } - - // ============================================================================================ - // ================================== SUBMISSION FORM ========================================= - // ============================================================================================ - - /** - * ??? - * - * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#get_form_elements - * @param $submissionorgrade - * @param MoodleQuickForm $mform - * @param stdClass $data - * @param $userid - * @return bool - * @throws coding_exception - */ - public function get_form_elements_for_user($submissionorgrade, - MoodleQuickForm $mform, - stdClass $data, - $userid): bool { - // TODO HR : Implement this function - // HR : Accepted files - $fileoptions = array( - 'subdirs' => 1, - "maxfiles" => 1, - 'accepted_types' => array(".zip"), - 'return_types' => FILE_INTERNAL - ); - - $submissionid = $submissionorgrade ? $submissionorgrade->id : 0; - - // HR : What does this code do ? - $data = file_prepare_standard_filemanager($data, - 'tasks', - $fileoptions, - $this->assignment->get_context(), - self::COMPONENT_NAME, - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA, - $submissionid); - - $name = get_string("form_filemanager", self::COMPONENT_NAME); - $element_id = "tasks_filemanager"; - $mform->addElement('filemanager', $element_id, $name, null, $fileoptions); - - $mform->addHelpButton( - $element_id, - "form_filemanager", - self::COMPONENT_NAME); - - return true; - } - - // ============================================================================================ - // ================================== INTERACTION WITH USER =================================== - // ============================================================================================ - - - /** - * @throws coding_exception - */ - public function save(stdClass $submissionorgrade, stdClass $data): bool { - // TODO HR : Implement this function - // This method will trigger the grading... - - // HR : Accepted files - $fileoptions = array( - 'subdirs' => 1, - "maxfiles" => 1, - 'accepted_types' => array(".zip"), - 'return_types' => FILE_INTERNAL - ); - - $submissionid = $submissionorgrade ? $submissionorgrade->id : 0; - - $data = file_prepare_standard_filemanager( - $data, - 'tasks', - $fileoptions, - $this->assignment->get_context(), - self::COMPONENT_NAME, - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA, - $submissionid - ); - - $fs = get_file_storage(); - - if($this->is_empty($submissionorgrade)){ - return true; - } - - $files = $fs->get_area_files( - $this->assignment->get_context()->id, - self::COMPONENT_NAME, - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA, - $submissionorgrade->id, - 'id', - false - ); - - // HR : Prepare curl request - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, 'http://grading.service:8082/api/v1/grading/grade'); - curl_setopt($curl, CURLOPT_POST, true); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_POSTFIELDS, $files); - - $headers = [ - 'Content-Type: application/octet-stream', - 'Content-Length: ' . strlen($files) - ]; - - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - - $response = curl_exec($curl); - - if($response === false){ - // TODO HR : Manage error here - \core\notification::error("Request failed !"); - return false; - } else { - \core\notification::info("Request worked !" . $response); - } - - curl_close($curl); - - return true; - } - - public function delete_instance(): bool { - // TODO HR : Implement this function - return false; - } - - // ============================================================================================ - // ==================================== FILE MANAGEMENT ======================================= - // ============================================================================================ - - /** - * ??? - * - * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#get_files - * @param stdClass $submissionorgrade - * @param stdClass $user - * @return array - * @throws coding_exception - */ - public function get_files(stdClass $submissionorgrade, stdClass $user): array { - $result = []; - $fs = get_file_storage(); - - $files = $fs->get_area_files( - $this->assignment->get_context()->id, - 'assignsubmission_file', - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA, - $submissionorgrade->id, - 'timemodified', - false - ); - - foreach ($files as $file) { - $result[$file->get_filename()] = $file; - } - return $result; - } - - public function get_file_areas(): array { - // TODO HR : Implement this function - return array( - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA => "ertzuijokp" - ); - } - - // ============================================================================================ - // ========================================= VIEWS ============================================ - // ============================================================================================ - - /** - * View of the summary (visible by the student) - * - * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#view_summary - * @param stdClass $submissionorgrade - * @param bool &$showviewlink - * @return string - * @throws coding_exception - */ - public function view_summary(stdClass $submissionorgrade, &$showviewlink): string { - $html = $this->assignment->render_area_files( - self::COMPONENT_NAME, - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA, - $submissionorgrade->id - ); - $html .= html_writer::tag("h5", get_string("student_view_summary_header", self::COMPONENT_NAME)); - - // TODO HR : Implement this function - // 1- Fetch the submission from the database - // 2- Process the submission - - return html_writer::div($html, "docker_submission_feedback_view"); - } - - public function view(stdClass $submissionorgrade): string { - // TODO HR : Implement this function - return ""; - } - - // ============================================================================================ - // ====================================== OTHERS ? ============================================ - // ============================================================================================ - - /** - * ??? - * - * @note See documentation : https://moodledev.io/docs/apis/plugintypes/assign/submission#is_empty - * @param stdClass $submissionorgrade - * @return bool - * @throws coding_exception - */ - public function is_empty(stdClass $submissionorgrade): bool { - // HR : Check if there is a file in the SUBMISSION_FILEAREA - /*$fs = get_file_storage(); - $files = $fs->get_area_files($this->assignment->get_context()->id, - self::COMPONENT_NAME, - ASSIGNSUBMISSION_DOCKER_SUBMISSION_FILEAREA, - $submissionorgrade, - 'id', - false); - */ - return false;//count($files) == 0; - } - -} \ No newline at end of file diff --git a/moodle-assignsubmission-autograding/settings.php b/moodle-assignsubmission-autograding/settings.php deleted file mode 100644 index e2c9ee589e3c57e860848f53c4e21925e3a094b5..0000000000000000000000000000000000000000 --- a/moodle-assignsubmission-autograding/settings.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php - -defined('MOODLE_INTERNAL') || die(); - -$settings->add( - new admin_setting_configtext("assignsubmission_docker_submission/service_url", - "Service URL", - "URL to the kubernetes cluster hosting the service", - "") -); \ No newline at end of file diff --git a/moodle-assignsubmission-autograding/version.php b/moodle-assignsubmission-autograding/version.php deleted file mode 100644 index b72d13ea976e3a1ce2cba5cc3a75235a99b33ff0..0000000000000000000000000000000000000000 --- a/moodle-assignsubmission-autograding/version.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php - -defined('MOODLE_INTERNAL') || die(); - -$plugin->version = 2023042400; -$plugin->requires = 2016052300; -$plugin->component = 'assignsubmission_docker_submission'; diff --git a/moodle-grading-service/pom.xml b/moodle-grading-service/pom.xml index 10300f4453e09a4094f105d514eff5b9658df60b..86fce6645636d79cd3b2adfa42dc609f8bc9ced4 100644 --- a/moodle-grading-service/pom.xml +++ b/moodle-grading-service/pom.xml @@ -21,17 +21,30 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> - <dependency> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - <optional>true</optional> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>io.fabric8</groupId> + <artifactId>kubernetes-client</artifactId> + <version>6.7.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <optional>true</optional> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/MoodleGradingServiceApplication.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/MoodleGradingServiceApplication.java index 34d70077e3d76d931956b5941df4c645b8ca8480..7820e620365573c0d94d713d3c158f2790a9b535 100644 --- a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/MoodleGradingServiceApplication.java +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/MoodleGradingServiceApplication.java @@ -3,9 +3,18 @@ package ch.epfl.cs107.grading.moodle; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ @SpringBootApplication public class MoodleGradingServiceApplication { + /** + * ??? + * @param args ??? + */ public static void main(String[] args) { SpringApplication.run(MoodleGradingServiceApplication.class, args); } diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/ApiKeyAuthentication.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/ApiKeyAuthentication.java new file mode 100644 index 0000000000000000000000000000000000000000..ce2e927f54b14d5e428eaec68fbcf3dfc62d1b84 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/ApiKeyAuthentication.java @@ -0,0 +1,69 @@ +package ch.epfl.cs107.grading.moodle.api.v1.auth; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +public final class ApiKeyAuthentication implements Authentication { + + /** ??? */ + private boolean authenticated; + + /** ??? */ + private final String key; + + /** + * ??? + * @param key ??? + * @param authenticated ??? + */ + public ApiKeyAuthentication(String key, boolean authenticated) { + this.key = key; + this.authenticated = authenticated; + } + + public String getKey(){ + return key; + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getDetails() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + this.authenticated = isAuthenticated; + } + + @Override + public String getName() { + return null; + } +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/filter/ApiKeyAuthenticationFilter.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/filter/ApiKeyAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..6299a1e247073b90f2146922535a99ac298de2a4 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/filter/ApiKeyAuthenticationFilter.java @@ -0,0 +1,58 @@ +package ch.epfl.cs107.grading.moodle.api.v1.auth.filter; + +import ch.epfl.cs107.grading.moodle.api.v1.auth.ApiKeyAuthentication; +import ch.epfl.cs107.grading.moodle.api.v1.auth.manager.ApiKeyAuthenticationManager; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Component +public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { + + private static final String API_KEY_HEADER_ENTRY = "API-KEY"; + + /** ??? */ + private final ApiKeyAuthenticationManager manager; + + /** + * ??? + * @param manager ??? + */ + @Autowired + public ApiKeyAuthenticationFilter(ApiKeyAuthenticationManager manager) { + this.manager = manager; + } + + /** + * ??? + * @param request ??? + * @param response ??? + * @param filterChain ??? + * @throws ServletException ??? + * @throws IOException ??? + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // HR : Fetch the API-KEY from the header + var key = request.getHeader(API_KEY_HEADER_ENTRY); + // HR : Request the authentication from the authentication manager + var auth = manager.authenticate(new ApiKeyAuthentication(key, false)); + // HR : Set the authentication in the SecurityContext + SecurityContextHolder.getContext().setAuthentication(auth); + // HR : Complete the chain of filters + filterChain.doFilter(request, response); + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/manager/ApiKeyAuthenticationManager.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/manager/ApiKeyAuthenticationManager.java new file mode 100644 index 0000000000000000000000000000000000000000..9abe18ad5eaf490afbf3a596862dc03610e6451c --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/manager/ApiKeyAuthenticationManager.java @@ -0,0 +1,41 @@ +package ch.epfl.cs107.grading.moodle.api.v1.auth.manager; + +import ch.epfl.cs107.grading.moodle.api.v1.auth.provider.ApiKeyAuthenticationProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Component +public class ApiKeyAuthenticationManager implements AuthenticationManager { + + /** ??? */ + private final ApiKeyAuthenticationProvider provider; + + /** + * ??? + * @param provider ??? + */ + @Autowired + public ApiKeyAuthenticationManager(ApiKeyAuthenticationProvider provider) { + this.provider = provider; + } + + /** + * ??? + * @param authentication the authentication request object + * @return ??? + * @throws AuthenticationException ??? + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return provider.authenticate(authentication); + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/provider/ApiKeyAuthenticationProvider.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/provider/ApiKeyAuthenticationProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..5837f16a3f524be18fbac061a286faccf7370043 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/auth/provider/ApiKeyAuthenticationProvider.java @@ -0,0 +1,60 @@ +package ch.epfl.cs107.grading.moodle.api.v1.auth.provider; + +import ch.epfl.cs107.grading.moodle.api.v1.auth.ApiKeyAuthentication; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import static java.util.Objects.isNull; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Component +public class ApiKeyAuthenticationProvider implements AuthenticationProvider { + + /** ??? */ + private final String apiKey; + + /** + * ??? + * @param apiKey ??? + */ + public ApiKeyAuthenticationProvider(@Value("${api.key}") String apiKey) { + this.apiKey = apiKey; + } + + + /** + * {@inheritDoc} + * @param authentication the authentication request object. + * @return ??? + * @throws AuthenticationException ??? + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + var keyAuth = (ApiKeyAuthentication) authentication; + if (isNull(keyAuth.getKey())) + return authentication; + if (keyAuth.getKey().equals(apiKey)) + return new ApiKeyAuthentication(keyAuth.getKey(), true); + else + throw new BadCredentialsException("API KEY IS NOT CORRECT"); + } + + /** + * {@inheritDoc} + * @param authenticationType ??? + * @return ??? + */ + @Override + public boolean supports(Class<?> authenticationType) { + return ApiKeyAuthentication.class.equals(authenticationType); + } +} + 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 new file mode 100644 index 0000000000000000000000000000000000000000..01d92938931f5fefcbc2e36063e7649e5e595b87 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/KubernetesConfig.java @@ -0,0 +1,15 @@ +package ch.epfl.cs107.grading.moodle.api.v1.config; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KubernetesConfig { + + @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 new file mode 100644 index 0000000000000000000000000000000000000000..8db96b3ddf255815a7497551812ade10cbd4a5cd --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/config/SecurityConfig.java @@ -0,0 +1,61 @@ +package ch.epfl.cs107.grading.moodle.api.v1.config; + +import ch.epfl.cs107.grading.moodle.api.v1.auth.filter.ApiKeyAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + /** ??? */ + private final ApiKeyAuthenticationFilter apiKeyAuthFilter; + + /** + * ??? + * @param apiKeyAuthFilter ??? + */ + @Autowired + public SecurityConfig(ApiKeyAuthenticationFilter apiKeyAuthFilter) { + this.apiKeyAuthFilter = apiKeyAuthFilter; + } + + /** + * ??? + * @param http ??? + * @return ??? + * @throws Exception ??? + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + // HR : Add the ApiKeyAuthentication filter + .addFilterAfter(apiKeyAuthFilter, BasicAuthenticationFilter.class) + // HR : Disable CSRF + .csrf(AbstractHttpConfigurer::disable) + // HR : Configure the end points authorizations + .authorizeHttpRequests(auth -> { + // HR : Allow to ping the no-auth end point + auth.requestMatchers("/api/v1/ping/no-auth").permitAll(); + // HR : Request authentication to ping the auth end point + auth.requestMatchers("/api/v1/ping/auth").authenticated(); + // HR : By default, request authentication to access any end point + auth.anyRequest().authenticated(); + }) + // HR : Build the SecurityFilterChain + .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 aca83b8e193cefc55873bbcb7462a5ef06ccb370..bdc4c6f14208b0980783284e36872d5ebff1438b 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,20 +1,108 @@ 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.service.KubernetesJobService; +import ch.epfl.cs107.grading.moodle.api.v1.service.MoodleWebService; +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. + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ @RestController @RequestMapping("/api/v1/grading") public 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; + + @Autowired + public GradingController(MoodleWebService moodleWebService, KubernetesJobService kubernetesJobService) { + this.moodleWebService = moodleWebService; + this.kubernetesJobService = kubernetesJobService; + } + + /** + * Handles grading requests from moodle. Creates a grading job on the kubernetes + * cluster. + * + * @param request ??? + * @return ??? + */ @PostMapping("/grade") - public ResponseEntity<String> ping(){ - try{ - return ResponseEntity.ok("File received !"); - } catch (Exception e){ - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + public ResponseEntity<?> grade(@RequestBody GradeRequestDTO request) { + + logger.info("Grading request received: {}", request.toString()); + + try { + var jobName = kubernetesJobService.createJob( + request.getUserid(), request.getCourseid(), + request.getAssignmentid(), request.getSubmissionid(), + request.getImageName(), request.getRegistryCredential()); + return ResponseEntity.ok(jobName); + } catch (RuntimeException e) { + logger.error("Failed to create grading job", e); + return ResponseEntity.internalServerError().build(); } } + /** + * 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 feedback ??? + */ + @PostMapping("/result") + public ResponseEntity<?> submitGrade( + @RequestParam int userid, + @RequestParam int courseid, + @RequestParam int assignmentid, + @RequestParam int submissionid, + @RequestBody String feedback + ) { + logger.info("Grade/feedback submitted by job: " + + "userid=" + userid + ", courseid=" + courseid + + ", assignmentid=" + assignmentid + ", submissionid=" + submissionid + ); + + try { + // HR : Upload grade and feedback to moodle + var res = moodleWebService.uploadAutoGradeFeedback( + userid, courseid, assignmentid, submissionid, + 74, feedback); + + 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=" + userid + ", courseid=" + courseid + + ", assignmentid=" + assignmentid + ", submissionid=" + submissionid, + e + ); + } + + 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 605eedb9dbfec726060d8dc32268f86247330c8e..f9104bb578f28448db7d3a81ff4158f241d5672b 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 @@ -1,16 +1,42 @@ package ch.epfl.cs107.grading.moodle.api.v1.controller; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ @RestController -@RequestMapping("/api/v1") +@RequestMapping("/api/v1/ping") public class PingController { - @GetMapping("/ping") - public String ping(){ - return "Hello from Spring boot"; + private Logger logger = LoggerFactory.getLogger(PingController.class); + + /** + * ??? + * + * @return ??? + */ + @GetMapping("/no-auth") + public String pingWithoutAuth() { + logger.info("Received a ping without auth"); + return "Hello from Spring boot - No Auth mode"; + } + + /** + * ??? + * + * @return ??? + */ + @GetMapping("/auth") + public String pingWithAuth() { + logger.info("Received a ping"); + return "Hello from Spring boot - 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 new file mode 100644 index 0000000000000000000000000000000000000000..c02c41a13b73d2c0d1c512a5ec1eea35e752a12e --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/RegistryCredentialsController.java @@ -0,0 +1,52 @@ +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.MoodleWebService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@RestController +@RequestMapping("/api/v1/registry/credentials") +public final class RegistryCredentialsController { + + /** ??? */ + private final KubernetesJobService k8s; + + /** ??? */ + private final MoodleWebService moodle; + + /** + * ??? + * @param k8s ??? + */ + @Autowired + public RegistryCredentialsController(KubernetesJobService k8s, MoodleWebService moodle) { + this.k8s = k8s; + this.moodle = moodle; + } + + /** + * ??? + * @return ??? + */ + @PostMapping("/upload") + public ResponseEntity<?> uploadCredentials(@RequestBody UploadCredentialsDTO credentials){ + try(var cred = moodle.download_credentials(credentials.getCourseid(), credentials.getAssignmentid())){ + // TODO : Use k8s to store the credentials in the corresponding namespace in the cluster + return ResponseEntity.ok().body(null); + } catch (Exception e){ + return ResponseEntity.internalServerError().body(e); + } + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..d314ac31da75a2c1ef0771ccbe6f444cc8a315f0 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/controller/SubmissionController.java @@ -0,0 +1,77 @@ +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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@RestController +@RequestMapping("/api/v1/submission") +public class SubmissionController { + + /** ??? */ + private final MoodleWebService moodle; + + private final SubmissionIntegrityService integrity; + + /** + * ??? + * @param moodle ??? + */ + @Autowired + public SubmissionController(MoodleWebService moodle, SubmissionIntegrityService integrity){ + this.moodle = moodle; + this.integrity = integrity; + } + + + /** + * ??? + * @return ??? + */ + @GetMapping("/download") + public ResponseEntity<?> download( + @RequestParam int courseid, + @RequestParam int submissionid, + @RequestParam String signature){ + // 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 !"); + // HR : Serve the actual file, the integrity of the signature was checked + try(var file = moodle.download_submission(courseid, submissionid)) { + // 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); + // HR : Build response + return ResponseEntity + .ok() + .headers(headers) + .body(body); + } catch (URISyntaxException | InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/DownloadCredentialsDTO.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/DownloadCredentialsDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ebb9d6f3923d24815460e37de43c398c57b3c6b1 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/DownloadCredentialsDTO.java @@ -0,0 +1,23 @@ +package ch.epfl.cs107.grading.moodle.api.v1.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Data +public class DownloadCredentialsDTO { + + /** ??? */ + private final String content; + + @JsonCreator + public DownloadCredentialsDTO(@JsonProperty("content") String content) { + this.content = content; + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/DownloadSubmissionDTO.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/DownloadSubmissionDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..2d607f640c78b77f1812e3d9de3f6c9fae574042 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/DownloadSubmissionDTO.java @@ -0,0 +1,14 @@ +package ch.epfl.cs107.grading.moodle.api.v1.dto; + +import lombok.Data; + +/** + * ??? + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Data +public final class DownloadSubmissionDTO { + + private final String content; + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..8288b825a8b5d4da8d41f6fa0afc66694f7718a5 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/GradeRequestDTO.java @@ -0,0 +1,29 @@ +package ch.epfl.cs107.grading.moodle.api.v1.dto; + +import lombok.Data; + +/** + * ??? + */ +@Data +public class GradeRequestDTO { + + /** ??? */ + private final int userid; + + /** ??? */ + private final int courseid; + + /** ??? */ + private final int assignmentid; + + /** ??? */ + private final int submissionid; + + /** ??? */ + private final String imageName; + + /** ??? */ + private final String registryCredential; + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..e1c2bd78fd993d3614bed8bd3200c8f5203cc26d --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/dto/UploadCredentialsDTO.java @@ -0,0 +1,19 @@ +package ch.epfl.cs107.grading.moodle.api.v1.dto; + +import lombok.Data; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Data +public final class UploadCredentialsDTO { + + /** ???? */ + private final int courseid; + + private final int assignmentid; + + +} 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/KubernetesJobService.java new file mode 100644 index 0000000000000000000000000000000000000000..9184592f74f0068cc0651ec6fdf3ea2c57e94c95 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/KubernetesJobService.java @@ -0,0 +1,343 @@ +package ch.epfl.cs107.grading.moodle.api.v1.service; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.batch.v1.Job; +import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder; +import io.fabric8.kubernetes.api.model.batch.v1.JobSpecBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Base64; +import java.util.Map; +import java.util.UUID; + +/** + * ??? + * @author Dixit Sabharwal (dixit.sabharwal@epfl.ch) + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@Service +public final class KubernetesJobService { + + /** ??? */ + private final Logger logger = LoggerFactory.getLogger(KubernetesJobService.class); + + /** ??? */ + private final KubernetesClient kubernetesClient; + + /** ??? */ + private final SubmissionIntegrityService subIntegrity; + + @Value("${grading.service.name}") + private String gradingServiceName; + @Value("${server.port}") + private int gradingServicePort; + @Value("${api.key}") + private String key; + + @Autowired + public KubernetesJobService(KubernetesClient kubernetesClient, SubmissionIntegrityService subIntegrity) { + this.kubernetesClient = kubernetesClient; + this.subIntegrity = subIntegrity; + } + + public String createJob( + int userid, int courseid, + int assignmentid, int submissionid, + String imageName, String registryCredential + ) throws RuntimeException { + + // TODO HR : That is not correct. the id for CS-107 + // TODO HR: is must likely not 107. + // TODO HR : what are the namespaces in k8s ? + var namespace = String.format("cs%d", courseid); + // Generate the job name + var jobName = generate_job_name(); + // Get secret for pulling image + var imagePullSecretName = tryCreateImagePullSecret(namespace, registryCredential); + // Create an ephemeral volume for holding the submission + var volume = prepare_job_volume(jobName); + // Create an InitContainer to download the zip file into the ephemeral volume + var initContainer = prepare_init_container( + jobName, + volume, + download_submission_command(courseid, submissionid) + ); + // Create container that will run the grading script + var gradingContainer = prepare_grading_container( + jobName, + imageName, + volume + ); + // Create a ReportingContainer to report the grading results + var reportingContainer = prepare_feedback_container( + courseid, + userid, + assignmentid, + submissionid, + volume + ); + + // Define job + var job = prepare_job( + jobName, volume, imagePullSecretName, + initContainer, gradingContainer, reportingContainer, + userid, assignmentid, submissionid + ); + + // Create job + try { + kubernetesClient + .batch() + .v1() + .jobs() + .inNamespace(namespace) + .resource(job) + .create(); + } catch (KubernetesClientException e) { + logger.error("Failed to create job {}", jobName, e); + throw new RuntimeException("Failed to create job"); + } + + return jobName; + } + + public String tryCreateImagePullSecret(String namespace, String registryCredential) + throws RuntimeException { + // Hash parameters to generate secret name (to avoid duplicates) + String secretName = "regcred-" + registryCredential.hashCode(); + + // Check if secret exists + Secret secret = null; + try { + secret = kubernetesClient.secrets().inNamespace(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"); + } + + if (secret != null) { + return secretName; + } + + // Create secret if it does not exist + Secret regCred = new SecretBuilder() + .withApiVersion("v1") + .withKind("Secret") + .withType("kubernetes.io/dockerconfigjson") + .withMetadata(new ObjectMetaBuilder() + .withName(secretName) + .build()) + .withData(Map.of( + ".dockerconfigjson", Base64.getEncoder().encodeToString(registryCredential.getBytes()))) + .build(); + + try { + kubernetesClient.secrets().inNamespace(namespace).resource(regCred).create(); + } catch (KubernetesClientException e) { + logger.error("Failed to create secret {}", secretName, e); + throw new RuntimeException("Failed to create imagePullSecret"); + } + + return secretName; + } + + // ============================================================================================ + // =================================== HELPER FUNCTIONS ======================================= + // ============================================================================================ + + /** + * ??? + * @return ??? + */ + private String generate_job_name(){ + return UUID.randomUUID().toString(); + } + + /** + * ??? + * @param job_name ??? + * @return ??? + */ + private Volume prepare_job_volume(String job_name){ + var volumeName = job_name + "-volume"; + return new VolumeBuilder() + .withName(volumeName) + .withEmptyDir( + new EmptyDirVolumeSourceBuilder() + .withNewSizeLimit("200Mi") + .build()) + .build(); + } + + /** + * ??? + * @param job_name ??? + * @param volume ??? + * @param cmd ??? + * @return ??? + */ + private Container prepare_init_container(String job_name, Volume volume, String cmd){ + return new ContainerBuilder() + .withName(job_name + "-init") + .withImage("busybox") + .withCommand("sh", "-c", cmd) + .withVolumeMounts(new VolumeMountBuilder() + .withName(volume.getName()) + .withMountPath("/data") + .build()) + .withResources(new ResourceRequirementsBuilder() + .withRequests(Map.of( + "cpu", new Quantity("100m"), + "memory", new Quantity("200Mi"))) + .withLimits(Map.of( + "cpu", new Quantity("100m"), + "memory", new Quantity("500Mi"))) + .build()) + .build(); + } + + /** + * ??? + * @param job_name ??? + * @param image_name ??? + * @param volume ??? + * @return ??? + */ + private Container prepare_grading_container(String job_name, String image_name, Volume volume){ + return new ContainerBuilder() + .withName(job_name + "-grader") + .withImage(image_name) + .withImagePullPolicy("Always") + .withVolumeMounts(new VolumeMountBuilder() + .withName(volume.getName()) + .withMountPath("/data") + .build()) + .withResources(new ResourceRequirementsBuilder() + .withRequests(Map.of( + "cpu", new Quantity("200m"), + "memory", new Quantity("500Mi"))) + .withLimits(Map.of( + "cpu", new Quantity("500m"), + "memory", new Quantity("1Gi"))) + .build()) + .build(); + } + + /** + * ??? + * @param courseid ??? + * @param userid ??? + * @param assignmentid ??? + * @param submissionid ??? + * @param volume ??? + * @return ??? + */ + private Container prepare_feedback_container( + int courseid, int userid, + int assignmentid, int submissionid, + Volume volume + ){ + return new ContainerBuilder() + .withName("reporting-container") + .withImage("appropriate/curl") + .withCommand( + "sh", "-c", + "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?", gradingServiceName, gradingServicePort) + + String.format("userid=%d&courseid=%d&assignmentid=%d&submissionid=%d", userid, courseid, assignmentid, submissionid) + + "\"" + ) + .withVolumeMounts(new VolumeMountBuilder() + .withName(volume.getName()) + .withMountPath("/data") + .build()) + .withEnv(new EnvVarBuilder() + .withName("API_KEY") + .withValueFrom(new EnvVarSourceBuilder() + .withNewSecretKeyRef() + .withName("grading-service-variables") + .withKey("API_KEY") + .endSecretKeyRef() + .build()) + .build()) + .withResources(new ResourceRequirementsBuilder() + .withRequests(Map.of( + "cpu", new Quantity("100m"), + "memory", new Quantity("200Mi"))) + .withLimits(Map.of( + "cpu", new Quantity("100m"), + "memory", new Quantity("200Mi"))) + .build()) + .build(); + } + + /** + * ??? + * @param job_name ??? + * @param volume ??? + * @param credentials ??? + * @param initContainer ??? + * @param gradingContainer ??? + * @param reportingContainer ??? + * @param userid ??? + * @param assignmentid ??? + * @param submissionid ??? + * @return ??? + */ + private Job prepare_job( + String job_name, Volume volume, String credentials, + Container initContainer, Container gradingContainer, Container reportingContainer, + int userid, int assignmentid, int submissionid + ){ + return new JobBuilder() + .withApiVersion("batch/v1") + .withKind("Job") + .withMetadata(new ObjectMetaBuilder() + .withName(job_name) + .withLabels(Map.of( + "userid", String.valueOf(userid), + "assignmentid", String.valueOf(assignmentid), + "submissionid", String.valueOf(submissionid) + )) + .build()) + .withSpec(new JobSpecBuilder() + .withTemplate(new PodTemplateSpecBuilder() + .withSpec(new PodSpecBuilder() + .withRestartPolicy("Never") + .addNewImagePullSecret(credentials) + .withInitContainers(initContainer, gradingContainer) + .withContainers(reportingContainer) + .withVolumes(volume) + .build()) + .build()) + .build()) + .build(); + } + + /** + * ??? + * @param courseid ??? + * @param submissionid ??? + * @return ??? + */ + private String download_submission_command(int courseid, int submissionid){ + var mac = subIntegrity.generate(courseid, submissionid); + // HR : The double \\& is important here since we are building a command + // HR : and &-only will be interpreted as two different unix commands. + return String.format("wget --header=\"API-KEY: %s\" \"http://%s:%d/api/v1/submission/download?courseid=%d&submissionid=%d&signature=%s\" -O /data/submission.zip", + key, + gradingServiceName, gradingServicePort, + courseid, 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 new file mode 100644 index 0000000000000000000000000000000000000000..8aba00212b68d2fe3b191d6f067161b976449fd8 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/MoodleWebService.java @@ -0,0 +1,188 @@ +package ch.epfl.cs107.grading.moodle.api.v1.service; + +import ch.epfl.cs107.grading.moodle.api.v1.dto.DownloadCredentialsDTO; +import ch.epfl.cs107.grading.moodle.api.v1.dto.DownloadSubmissionDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import static java.net.URLEncoder.encode; +import static java.net.http.HttpResponse.BodyHandlers.ofString; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * ??? + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + * @since 1.0.0 + */ +@Service +public final class MoodleWebService { + + /** ??? */ + private final static String MOODLE_WEB_SERVICE_API = "/webservice/rest/server.php"; + + /** ??? */ + private final String MOODLE_BASE_URL; + + /** ??? */ + private final String MOODLE_ACCESS_TOKEN; + + /** + * ??? + * @param url ??? + * @param token ??? + */ + public MoodleWebService(@Value("${moodle.baseurl}")String url, @Value("${moodle.autograde.token}") String token) { + this.MOODLE_BASE_URL = url; + this.MOODLE_ACCESS_TOKEN = token; + } + + // ============================================================================================ + // ===================================== GENERIC FUNCTION ===================================== + // ============================================================================================ + + /** + * ??? + * @param function ??? + * @param params ??? + * @return ??? + * @throws URISyntaxException ??? + * @throws IOException ??? + * @throws InterruptedException ??? + */ + private <T> HttpResponse<T> call(String function, Map<String, Object> params, HttpResponse.BodyHandler<T> handler) + throws URISyntaxException, IOException, InterruptedException { + + var client = HttpClient.newHttpClient(); + + // HR : Build the URL + var sb = new StringBuilder(MOODLE_BASE_URL); + sb.append(MOODLE_WEB_SERVICE_API); // HR : End point of moodle + sb.append('?'); + sb.append("wstoken=").append(MOODLE_ACCESS_TOKEN); + sb.append("&wsfunction=").append(function); + sb.append("&moodlewsrestformat=json"); + for (var param : params.entrySet()) { + sb.append('&') + .append(encode(param.getKey(), UTF_8)) + .append('=') + .append(encode(param.getValue().toString(), UTF_8)); + } + + // HR : Build the request + var request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.noBody()) + .header("Content-Type", "application/json") + .uri(new URI(sb.toString())) + .build(); + // HR : Send the request to moodle and wait for the response + return client.send(request, handler); + } + + /** + * ??? + * @param userid ??? + * @param courseid ??? + * @param assignmentid ??? + * @param submissionid ??? + * @param grade ??? + * @param feedback ??? + * @return ??? + * @throws URISyntaxException ??? + * @throws IOException ??? + * @throws InterruptedException ??? + */ + public HttpResponse<?> uploadAutoGradeFeedback( + int userid, + int courseid, + int assignmentid, + int submissionid, + int grade, + String feedback + ) throws URISyntaxException, IOException, InterruptedException { + 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("submissionid", submissionid); + params.put("grade", grade); + params.put("feedback", feedback); + // HR : Call the moodle web service + return call(FUNCTION_NAME, params, ofString()); + } + + /** + * ??? + * @param courseid ??? + * @param submissionid ??? + * @return ??? + * @throws URISyntaxException ??? + * @throws IOException ??? + * @throws InterruptedException ??? + */ + public InputStream download_submission(int courseid, int submissionid) throws URISyntaxException, IOException, InterruptedException { + final var FUNCTION_NAME = "mod_assignsubmission_autograde_download_submission"; + + final var params = new HashMap<String, Object>(); + params.put("courseid", courseid); + params.put("submissionid", submissionid); + + var response = call(FUNCTION_NAME, params, ofString(UTF_8)); + + var objectMapper = new ObjectMapper(); + var submission = objectMapper.readValue(response.body(), DownloadSubmissionDTO.class); + + if(response.statusCode() != HttpStatus.OK.value()){ + throw new IllegalStateException(submission.getContent()); + } else { + return new ByteArrayInputStream(Base64.getDecoder().decode(submission.getContent())); + } + } + + /** + * ??? + * @param courseid ??? + * @param assignmentid ??? + * @return ??? + * @throws URISyntaxException ??? + * @throws IOException ??? + * @throws InterruptedException ??? + */ + public InputStream download_credentials(int courseid, int assignmentid) throws URISyntaxException, IOException, InterruptedException { + final var FUNCTION_NAME = "mod_assignsubmission_autograde_download_credentials"; + + final var params = new HashMap<String, Object>(); + params.put("courseid", courseid); + params.put("assignmentid", assignmentid); + + var response = call(FUNCTION_NAME, params, ofString()); + + System.out.println(response.body()); + + var objectMapper = new ObjectMapper(); + var submission = objectMapper.readValue(response.body(), DownloadCredentialsDTO.class); + + if(response.statusCode() != HttpStatus.OK.value()){ + throw new IllegalStateException(submission.getContent()); + } else { + return new ByteArrayInputStream(Base64.getDecoder().decode(submission.getContent())); + } + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..1fba9f63150938c06ad7e30def3e9fbfe38609fe --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/api/v1/service/SubmissionIntegrityService.java @@ -0,0 +1,69 @@ +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); + } + +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/logging/MdcInterceptor.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/logging/MdcInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..5ffba36b2a8d8ec19f499119985e31376ae01d84 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/logging/MdcInterceptor.java @@ -0,0 +1,30 @@ +package ch.epfl.cs107.grading.moodle.logging; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.UUID; + +/** + * Interceptor that adds a unique request id to the MDC, + * so that it can be used in the logs to track a request through the system. + * + * @implNote To work with async requests, you need to create a TaskDecorator that copies the MDC to the new thread. + */ +@Component +public class MdcInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + MDC.put("requestId", UUID.randomUUID().toString()); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + MDC.remove("requestId"); + } +} diff --git a/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/logging/WebMvcConfiguration.java b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/logging/WebMvcConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..607297481d971db9076bea826e2c4bd7aab54385 --- /dev/null +++ b/moodle-grading-service/src/main/java/ch/epfl/cs107/grading/moodle/logging/WebMvcConfiguration.java @@ -0,0 +1,22 @@ +package ch.epfl.cs107.grading.moodle.logging; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Component +public class WebMvcConfiguration implements WebMvcConfigurer { + + private final MdcInterceptor mdcInterceptor; + + @Autowired + public WebMvcConfiguration(MdcInterceptor mdcInterceptor) { + this.mdcInterceptor = mdcInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(mdcInterceptor); + } +} diff --git a/moodle-grading-service/src/main/resources/logback-spring.xml b/moodle-grading-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..647364622402c917514e965043a496dce9335440 --- /dev/null +++ b/moodle-grading-service/src/main/resources/logback-spring.xml @@ -0,0 +1,11 @@ +<configuration> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{ISO8601} %X{requestId} [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="STDOUT"/> + </root> +</configuration> diff --git a/moodle-grading-service/src/test/java/ch/epfl/cs107/grading/moodle/controller/PingControllerTest.java b/moodle-grading-service/src/test/java/ch/epfl/cs107/grading/moodle/controller/PingControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5480d3736cd500929b006d982769e1e52917f547 --- /dev/null +++ b/moodle-grading-service/src/test/java/ch/epfl/cs107/grading/moodle/controller/PingControllerTest.java @@ -0,0 +1,78 @@ +package ch.epfl.cs107.grading.moodle.controller; + +import ch.epfl.cs107.grading.moodle.utils.WithApiKeyAuthentication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest +public final class PingControllerTest { + + @Value("${api.key}") + private String api_key; + + /** ??? */ + private final static String NO_AUTH_END_POINT = "/api/v1/ping/no-auth"; + + /** ???? */ + private final static String AUTH_END_POINT = "/api/v1/ping/auth"; + + /** ??? */ + @Autowired + private MockMvc mockMvc; + + /** + * ??? + * @throws Exception ??? + */ + @Test + void successPingNoAuthWithoutKey() throws Exception { + mockMvc.perform(get(NO_AUTH_END_POINT)) + .andExpect(status().isOk()); + } + + /** + * ??? + * @throws Exception ??? + */ + @Test + //@WithApiKeyAuthentication + void successPingNoAuthWithKey() throws Exception { + var headers = new HttpHeaders(); + headers.add("API-KEY", api_key); + mockMvc.perform(get(NO_AUTH_END_POINT).headers(headers)) + .andExpect(status().isOk()); + } + + /** + * ??? + * @throws Exception ??? + */ + @Test + void failPingAuthWithoutKey() throws Exception { + mockMvc.perform(get(AUTH_END_POINT)) + .andExpect(status().isForbidden()); + } + + /** + * ??? + * @throws Exception ??? + */ + @Test + //@WithApiKeyAuthentication + void successPingAuthWithKey() throws Exception { + var headers = new HttpHeaders(); + headers.add("API-KEY", api_key); + mockMvc.perform(get(AUTH_END_POINT).headers(headers)) + .andExpect(status().isOk()); + } + +} diff --git a/moodle-grading-service/src/test/java/ch/epfl/cs107/grading/moodle/utils/WithApiKeyAuthentication.java b/moodle-grading-service/src/test/java/ch/epfl/cs107/grading/moodle/utils/WithApiKeyAuthentication.java new file mode 100644 index 0000000000000000000000000000000000000000..4c9503b2aaa1005c9008adf47d4368bbaf832214 --- /dev/null +++ b/moodle-grading-service/src/test/java/ch/epfl/cs107/grading/moodle/utils/WithApiKeyAuthentication.java @@ -0,0 +1,39 @@ +package ch.epfl.cs107.grading.moodle.utils; + +import ch.epfl.cs107.grading.moodle.api.v1.auth.ApiKeyAuthentication; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContext; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +/** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ +@WithSecurityContext(factory = WithApiKeyAuthentication.WithApiKeyAuthenticationFactory.class) +public @interface WithApiKeyAuthentication { + + /** + * ??? + * + * @author Hamza REMMAL (hamza.remmal@epfl.ch) + */ + final class WithApiKeyAuthenticationFactory implements WithSecurityContextFactory<WithApiKeyAuthentication> { + + /** ??? */ + @Value("${api.key}") + private String API_KEY; + + @Override + public SecurityContext createSecurityContext(WithApiKeyAuthentication annotation) { + var authentication = new ApiKeyAuthentication(API_KEY, true); + var context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + return context; + } + + } + +} diff --git a/moodle-grading-service/src/test/resources/application.properties b/moodle-grading-service/src/test/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..5ba552783ba5081ff9d109ca5af7cd793a66f37f --- /dev/null +++ b/moodle-grading-service/src/test/resources/application.properties @@ -0,0 +1,8 @@ +# Set the port of the server +server.port=8082 + +# +api.key=123456789 +moodle.baseUrl=http://moodle:80 +moodle.autograde.token=123456789 +grading.service.name=grading-service