Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Exchange of Artemis programming exercises via CodeAbility Sharing Platform #9909

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .idea/runConfigurations/Artemis_Dev.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ spotless {
"src/test/java/**/*.java",
)
exclude(
"**/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java",
"**/src/test/resources/test-data/repository-export/EncodingISO_8559_1.java",
"**/node_modules/**",
"**/out/**",
Expand Down Expand Up @@ -217,6 +218,7 @@ repositories {

dependencies {

implementation group: 'org.codeability', name: 'SharingPluginPlatformAPI', version: '1.0.2'
// Note: jenkins-client is not well maintained and includes dependencies to libraries with critical security issues (e.g. CVE-2020-10683 for dom4j@1.6.1)
// implementation "com.offbytwo.jenkins:jenkins-client:0.3.8"
implementation files("libs/jenkins-client-0.4.1.jar")
Expand Down
1 change: 1 addition & 0 deletions docker/artemis-dev-local-vc-local-ci-mysql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
ports:
- "8080:8080"
- "5005:5005" # Java Remote Debugging port declared in the java cmd options
- "22:7921"
# expose the port to make it reachable docker internally even if the external port mapping changes
expose:
- "5005"
Expand Down
1 change: 1 addition & 0 deletions docker/artemis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
# either add them in the environments or env_file section (alternative to application-local.yml)
env_file:
- ./artemis/config/prod.env
- ./.env
# if you need to use another port than 8080 or one fixed port for all artemis-app containers in the future
# you will probably not be able to override this setting outside the artemis.yml
# as stated in the docker compose docs (at least not when this was committed)
Expand Down
2 changes: 1 addition & 1 deletion docker/artemis/config/dev-local-vc-local-ci.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# https://docs.artemis.cit.tum.de/dev/setup.html#debugging-with-docker
_JAVA_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

SPRING_PROFILES_ACTIVE: artemis,scheduling,localci,localvc,buildagent,core,dev,docker
SPRING_PROFILES_ACTIVE: artemis,scheduling,localci,localvc,buildagent,core,dev,docker,sharing

# Integrated Code Lifecycle settings with Jira
ARTEMIS_USERMANAGEMENT_USEEXTERNAL="false"
Expand Down
19 changes: 17 additions & 2 deletions src/main/java/de/tum/cit/aet/artemis/core/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,24 @@ public final class Constants {
public static final int PUSH_NOTIFICATION_VERSION = 1;

/**
* The value of the version field we send with each push notification to the native clients (Android & iOS).
* sharing configution resource path for sharing config request
*/
public static final String SHARINGCONFIG_RESOURCE_PATH = "/sharing/config";
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved

/**
* sharing configution resource path for sharing config import request
*/
public static final String SHARINGIMPORT_RESOURCE_PATH = "/sharing/import";

/**
* sharing configution resource path for sharing config export request
*/
public static final String SHARINGEXPORT_RESOURCE_PATH = "/sharing/export";

/**
* sharing configution resource path for rest request, iff sharing profile is enabled
*/
public static final int PUSH_NOTIFICATION_MINOR_VERSION = 2;
public static final String SHARINGCONFIG_RESOURCE_IS_ENABLED = SHARINGCONFIG_RESOURCE_PATH + "/is-enabled";

/**
* The directory in the docker container in which the build script is executed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, SecurityProblemSupport
.requestMatchers("/websocket/**").permitAll()
.requestMatchers("/.well-known/jwks.json").permitAll()
.requestMatchers("/.well-known/assetlinks.json").permitAll()
// sharing is protected by explicit security tokens, thus we can permitAll here
.requestMatchers("/api/sharing/**").permitAll()
// Prometheus endpoint protected by IP address.
.requestMatchers("/management/prometheus/**").access((authentication, context) -> new AuthorizationDecision(monitoringIpAddresses.contains(context.getRequest().getRemoteAddr())));

Expand Down
102 changes: 102 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/core/dto/SharingInfoDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package de.tum.cit.aet.artemis.core.dto;

import java.util.Objects;

import org.springframework.context.annotation.Profile;

/**
* the sharing info to request an specific exercise from the sharing platform.
*/
@Profile("sharing")
public class SharingInfoDTO {

/**
* the (random) basket Token
*/
private String basketToken;

/**
* the callback URL for the basket request
*/
private String returnURL;

/**
* the base URL for the basket request
*/
private String apiBaseURL;
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved

/**
* the index of the request exercise
*/
private int exercisePosition;
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved

/**
* the callback URL for the basket request
*/
public String getReturnURL() {
return returnURL;
}

/**
* sets the callback URL for the basket request
*/
public void setReturnURL(String returnURL) {
this.returnURL = returnURL;
}

/**
* the base URL for the basket request
*/
public String getApiBaseURL() {
return apiBaseURL;
}

/**
* sets the base URL for the basket request
*/
public void setApiBaseURL(String apiBaseURL) {
this.apiBaseURL = apiBaseURL;
}

/**
* the (random) basket Token
*/
public String getBasketToken() {
return basketToken;
}

/**
* sets the basket Token
*/
public void setBasketToken(String basketToken) {
this.basketToken = basketToken;
}

/**
* the index of the request exercise
*/
public int getExercisePosition() {
return exercisePosition;
}

/**
* sets the index of the request exercise
*/
public void setExercisePosition(int exercisePosition) {
this.exercisePosition = exercisePosition;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass())
return false;
SharingInfoDTO that = (SharingInfoDTO) o;
return exercisePosition == that.exercisePosition && Objects.equals(basketToken, that.basketToken) && Objects.equals(returnURL, that.returnURL)
&& Objects.equals(apiBaseURL, that.apiBaseURL);
}
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved

@Override
public int hashCode() {
return Objects.hash(basketToken, returnURL, apiBaseURL, exercisePosition);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,13 @@ public boolean isLtiActive() {
public boolean isProductionActive() {
return isProfileActive(JHipsterConstants.SPRING_PROFILE_PRODUCTION);
}

/**
* Checks if the sharing profile is active
*
* @return true if the sharing profile is active, false otherwise
*/
public boolean isSharing() {
return isProfileActive("sharing");
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package de.tum.cit.aet.artemis.core.web;

import static de.tum.cit.aet.artemis.core.config.Constants.SHARINGCONFIG_RESOURCE_IS_ENABLED;
import static de.tum.cit.aet.artemis.core.config.Constants.SHARINGCONFIG_RESOURCE_PATH;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Optional;

import org.codeability.sharing.plugins.api.SharingPluginConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.exercise.service.sharing.SharingConnectorService;

/**
* REST controller for the exchange of configuration data between artemis and the sharing platform.
*/
@Validated
@RestController
@RequestMapping("/api")
@Profile("sharing")
public class SharingSupportResource {

/**
* the logger
*/
private final Logger log = LoggerFactory.getLogger(SharingSupportResource.class);

/**
* the sharing plugin service
*/
private final SharingConnectorService sharingConnectorService;

/**
* constructor
*
* @param sharingConnectorService the sharing connector service
*/
@SuppressWarnings("unused")
public SharingSupportResource(SharingConnectorService sharingConnectorService) {
this.sharingConnectorService = sharingConnectorService;
}

/**
* Returns Sharing Plugin configuration to be used in context with Artemis.
* This configuration is requested by the sharing platform on a regular basis.
* It is secured by the common secret api key token transferred by Authorization header.
*
* @param sharingApiKey the common secret api key token (transfered by Authorization header).
* @param apiBaseUrl the base url of the sharing application api (for callbacks)
* @param installationName a descriptive name of the sharing application
*
* @return Sharing Plugin configuration
* @see <a href="https://sharing-codeability.uibk.ac.at/development/sharing/codeability-sharing-platform/-/wikis/Setup/Connector-Interface-Setup">Connector Interface Setup</a>
*
*/
@GetMapping(SHARINGCONFIG_RESOURCE_PATH)
public ResponseEntity<SharingPluginConfig> getConfig(@RequestHeader("Authorization") Optional<String> sharingApiKey, @RequestParam String apiBaseUrl,
@RequestParam String installationName) {
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved
if (sharingApiKey.isPresent() && sharingConnectorService.validate(sharingApiKey.get())) {
log.info("Delivered Sharing Config ");
URL apiBaseUrl1;
try {
apiBaseUrl1 = URI.create(apiBaseUrl).toURL();
}
catch (MalformedURLException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved
}
return ResponseEntity.ok(sharingConnectorService.getPluginConfig(apiBaseUrl1, installationName));
}
log.warn("Received wrong or missing api key");
return ResponseEntity.status(401).body(null);
}

/**
* Return a boolean value representing the current state of Sharing
*
* @return Status 200 if a Sharing ApiBaseUrl is present, Status 503 otherwise
*/
@GetMapping(SHARINGCONFIG_RESOURCE_IS_ENABLED)
public ResponseEntity<Boolean> isSharingEnabled() {
if (sharingConnectorService.isSharingApiBaseUrlPresent()) {
return ResponseEntity.status(200).body(true);
}
return ResponseEntity.status(503).body(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package de.tum.cit.aet.artemis.exercise.service.sharing;

import java.io.IOException;
import java.net.URISyntaxException;

import org.eclipse.jgit.api.errors.GitAPIException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportFromFileService;
import de.tum.cit.aet.artemis.sharing.ExerciseSharingService;
import de.tum.cit.aet.artemis.sharing.SharingMultipartZipFile;
import de.tum.cit.aet.artemis.sharing.SharingSetupInfo;

/**
* Service for importing programming exercises from the sharing service.
*/
@Service
@Profile("sharing")
public class ProgrammingExerciseImportFromSharingService {

/**
* the logger
*/
private final Logger log = LoggerFactory.getLogger(ProgrammingExerciseImportFromSharingService.class);

/**
* the import from file service (because this service strongly relies in the file import format.
*/
private final ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService;

/**
* the general exercise sharing service.
*/
private final ExerciseSharingService exerciseSharingService;

/**
* the user repository
*/
private final UserRepository userRepository;

/**
* constructor for spring initialization
*
* @param programmingExerciseImportFromFileService import from file services
* @param exerciseSharingService exercise sharing service
* @param userRepository user repository
*/
public ProgrammingExerciseImportFromSharingService(ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService,
ExerciseSharingService exerciseSharingService, UserRepository userRepository) {
this.programmingExerciseImportFromFileService = programmingExerciseImportFromFileService;
this.exerciseSharingService = exerciseSharingService;
this.userRepository = userRepository;
}

/**
* Imports a programming exercise from the Sharing platform.
* It reuses the implementation of ProgrammingExerciseImportFromFileService for importing the exercise from a Zip file.
*
* @param sharingSetupInfo Containing sharing and exercise data needed for the import
*/
public ProgrammingExercise importProgrammingExerciseFromSharing(SharingSetupInfo sharingSetupInfo) throws SharingException, IOException, GitAPIException, URISyntaxException {
SharingMultipartZipFile zipFile = exerciseSharingService.getCachedBasketItem(sharingSetupInfo.getSharingInfo());
User user = userRepository.getUserWithGroupsAndAuthorities();

if (sharingSetupInfo.getExercise().getCourseViaExerciseGroupOrCourseMember() == null) {
sharingSetupInfo.getExercise().setCourse(sharingSetupInfo.getCourse());
}
Wallenstein61 marked this conversation as resolved.
Show resolved Hide resolved
return this.programmingExerciseImportFromFileService.importProgrammingExerciseFromFile(sharingSetupInfo.getExercise(), zipFile, sharingSetupInfo.getCourse(), user, true);
}
}
Loading
Loading