From a5b21b18c68203652b144620f67c9a60d632a022 Mon Sep 17 00:00:00 2001
From: Gaofei Zhao <15748980+dippindots@users.noreply.github.com>
Date: Tue, 2 Jul 2024 22:50:51 -0400
Subject: [PATCH 1/9] Frontend v6.0.10
---
pom.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pom.xml b/pom.xml
index 5183f814d08..273c927ee8b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
org.cbioportal
cbioportal
- 6.0.10-SNAPSHOT
+ 6.0.10
cBioPortal for Cancer Genomics
@@ -27,7 +27,7 @@
com.github.cbioportal
- v6.0.9
+ v6.0.10
2.13.1
From 222696c8e1a98d8de23d958c7d8714276ddb36af Mon Sep 17 00:00:00 2001
From: Gaofei Zhao <15748980+dippindots@users.noreply.github.com>
Date: Tue, 2 Jul 2024 23:20:15 -0400
Subject: [PATCH 2/9] Prepare v6.0.11 release
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 273c927ee8b..51a51dddc53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
org.cbioportal
cbioportal
- 6.0.10
+ 6.0.11-SNAPSHOT
cBioPortal for Cancer Genomics
From 8e8f86bc4420624ed5f02a947271dc7bc81b5a94 Mon Sep 17 00:00:00 2001
From: Gaofei Zhao <15748980+dippindots@users.noreply.github.com>
Date: Tue, 9 Jul 2024 23:43:28 -0400
Subject: [PATCH 3/9] Frontend v6.0.11
---
pom.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pom.xml b/pom.xml
index 51a51dddc53..483c1ddc08f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
org.cbioportal
cbioportal
- 6.0.11-SNAPSHOT
+ 6.0.11
cBioPortal for Cancer Genomics
@@ -27,7 +27,7 @@
com.github.cbioportal
- v6.0.10
+ v6.0.11
2.13.1
From dc2c5944df60c46bcd80b78d6ee4bb0cbdda1a27 Mon Sep 17 00:00:00 2001
From: Gaofei Zhao <15748980+dippindots@users.noreply.github.com>
Date: Tue, 9 Jul 2024 23:44:23 -0400
Subject: [PATCH 4/9] Prepare v6.0.12 release
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 483c1ddc08f..b3866426742 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
org.cbioportal
cbioportal
- 6.0.11
+ 6.0.12-SNAPSHOT
cBioPortal for Cancer Genomics
From a4a5942f10fe614b6d31c8fbb14b09b428a10baf Mon Sep 17 00:00:00 2001
From: Ruslan Forostianov
Date: Wed, 10 Jul 2024 17:55:00 +0200
Subject: [PATCH 5/9] RFC83: Add admin call to make virtual study available for
all users on their landing pages (#10829)
* Implement endpoints for public virtual studies
* Add possibility to specify cancer type id and pmi for virt. study during publishing
* Filter out forbidden study ids from virtual studies
* Do not allow set * user for user virtual study
To prevent missuse of the request to publish virtual study for everyone!
* Add integration tests for (un)publishing virtual study
* Assert fields fo published virtual studies
* Use recommended ways to inject dependencies in spring
* Add issue link to session service FIXMEs
* Fix sonar reported NPE bugs
* Remove unnecessary checks for null
* Remove obsolete TODO comment
* Throw AccessForbiddenException and use GlobalExceptionHadler instead
* Throw IllegalStateException is downstream server return unsuccessful result
* Remove raw use of ResponseEntity
* Throw bad request exception instead of returning ResponseEntity
* Do not filter out VS when user does not hava access to underlying study samples
* Fix integration tests
* Extract http calls to the session service to the handler
* Remove todo comment
User has to have * username witch is not likely
* Fix sonarcloud issues
* Deduplicate ensuring publisher api key is correct
* Remove usage of generic wildcard type
* Extract logic to update VS metadata fields into a method
* Document publishing virtual study feature
* Update docs/Create-And-Publish-Virtual-Study.md
Co-authored-by: pieterlukasse
* Publish virtual study by modifying it instead of making copy
Also get rid of undocumented endpoint
* Improve name and docs of method to retrieve VS for user
* Assign VM after un-publshing to the owner
Make sure that we unpublish public virtual study. Fail otherwise.
---------
Co-authored-by: Charles Haynes
Co-authored-by: pieterlukasse
---
docs/Create-And-Publish-Virtual-Study.md | 61 +++++
docs/Data-Loading.md | 3 +
pom.xml | 10 +-
.../security/config/MethodSecurityConfig.java | 32 +--
.../exception/AccessForbiddenException.java | 7 +
.../util/SessionServiceRequestHandler.java | 88 +++++++
.../web/PublicVirtualStudiesController.java | 139 ++++++++++
.../web/SessionServiceController.java | 18 +-
.../web/error/GlobalExceptionHandler.java | 12 +
.../web/parameter/VirtualStudyData.java | 18 ++
.../resources/application.properties.EXAMPLE | 3 +
.../PublicVirtualStudiesIntegrationTest.java | 238 ++++++++++++++++++
12 files changed, 603 insertions(+), 26 deletions(-)
create mode 100644 docs/Create-And-Publish-Virtual-Study.md
create mode 100644 src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java
create mode 100644 src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java
create mode 100644 src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java
diff --git a/docs/Create-And-Publish-Virtual-Study.md b/docs/Create-And-Publish-Virtual-Study.md
new file mode 100644
index 00000000000..14ebbf9c438
--- /dev/null
+++ b/docs/Create-And-Publish-Virtual-Study.md
@@ -0,0 +1,61 @@
+# Create and Publish Virtual Study
+
+A [Virtual Study](./user-guide/faq.md#what-is-a-virtual-study) defines a subset or a combination of samples from one or more studies in the system.
+
+*Note*: To publish or un-publish a virtual study, your cBioPortal instance must be configured with `session.endpoint.publisher-api-key` in the `application.properties`.
+
+## Create Virtual Study
+
+To create a virtual study in cBioPortal, follow these steps:
+
+1. Define the desired filters on the study or multiple studies summary page.
+2. Click the button with the bookmark icon () in the top right corner of the screen.
+3. Provide a title and description, then click the Save button. You will see a link that looks like:
+
+```
+https:///study?id=
+```
+
+4. Save the virtual study link or ID if you want to publish it.
+
+If you are logged in, this virtual study will appear in the `My Virtual Studies` section on the landing page.
+You can always find the ID of the virtual study from the URL of the page that opens after clicking on it.
+
+## Publish Virtual Study
+
+To publish a virtual study, you need to supply the publisher API key in the `X-PUBLISHER-API-KEY` header.
+
+Here is a curl command to publish a virtual study:
+```shell
+curl \
+ -X POST \
+ -H 'X-PUBLISHER-API-KEY: ' \
+ -v 'http:///api/public_virtual_studies/'
+```
+The published virtual study will appear under the `Public Virtual Studies` section (next to the `My Virtual Studies` section) on the landing page for all users of cBioPortal.
+
+While publishing, you can specify the PubMed ID (`pmid`) and `typeOfCancerId` of the virtual study using the following command:
+```shell
+curl \
+ -X POST \
+ -H 'X-PUBLISHER-API-KEY: ' \
+ -v 'http:///api/public_virtual_studies/?pmid=&typeOfCancerId='
+```
+
+The type of cancer code should match the known types of cancers in the cBioPortal database.
+If the type of cancer is specified, the published virtual study will appear under the respective cancer section on the landing page.
+Specifying the `pmid` enables a link to the PubMed page of the study.
+
+## Un-publish Virtual Study
+
+To un-publish a virtual study, you need to supply the publisher API key in the `X-PUBLISHER-API-KEY` header.
+After un-publishing, virtual study will no longer be displayed in the `Public Virtual Studies` section on the landing page.
+However, it reappears in the `My Virtual Studies` section for the owner.
+
+Here is the command to un-publish a virtual study:
+```shell
+curl \
+ -X DELETE \
+ -H 'X-PUBLISHER-API-KEY: ' \
+ -v 'http:///api/public_virtual_studies/'
+```
\ No newline at end of file
diff --git a/docs/Data-Loading.md b/docs/Data-Loading.md
index 39f27d9b385..9465f051edb 100644
--- a/docs/Data-Loading.md
+++ b/docs/Data-Loading.md
@@ -58,3 +58,6 @@ To remove a study, the [cbioportalImporter script](/Data-Loading-Maintaining-Stu
## Example studies
Examples for the different types of data are available on the [File Formats](/File-Formats.md) page. The Provisional TCGA studies, downloadable from the [Data Sets section](https://www.cbioportal.org/datasets) are complete studies that can be used as reference when creating data files.
+
+## Public Virtual Studies
+If your new study data is a subset or a combination of existing studies in the system, consider using [Public Virtual Studies](./Create-And-Publish-Virtual-Study.md) instead of duplicating data.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index b3866426742..20936fa44d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -98,6 +98,7 @@
3.14.0
4.17.0
7.1.0
+ 5.2.1
@@ -346,7 +347,14 @@
sentry-spring-boot-starter-jakarta
${sentry.version}
-
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ ${apache_httpclient.version}
+ test
+
+
+
diff --git a/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java b/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java
index f1171fd48db..c6bd8d04645 100644
--- a/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java
+++ b/src/main/java/org/cbioportal/security/config/MethodSecurityConfig.java
@@ -2,12 +2,10 @@
import org.cbioportal.persistence.cachemaputil.CacheMapUtil;
import org.cbioportal.security.CancerStudyPermissionEvaluator;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -17,28 +15,22 @@
// We are allowing users to enable method_authorization if optional_oauth2 is selected
@ConditionalOnExpression("{'oauth2','saml', 'saml_plus_basic'}.contains('${authenticate}') or ('optional_oauth2' eq '${authenticate}' and 'true' eq '${security.method_authorization_enabled}')")
public class MethodSecurityConfig {
- @Value("${app.name:}")
- private String appName;
-
- @Value("${filter_groups_by_appname:true}")
- private String doFilterGroupsByAppName;
-
- @Value("${always_show_study_group:}")
- private String alwaysShowCancerStudyGroup;
-
- @Autowired
- private CacheMapUtil cacheMapUtil;
@Bean
- public MethodSecurityExpressionHandler createExpressionHandler() {
+ public CancerStudyPermissionEvaluator cancerStudyPermissionEvaluator(
+ @Value("${app.name:}") String appName,
+ @Value("${filter_groups_by_appname:true}") String doFilterGroupsByAppName,
+ @Value("${always_show_study_group:}") String alwaysShowCancerStudyGroup,
+ CacheMapUtil cacheMapUtil
+ ) {
+ return new CancerStudyPermissionEvaluator(appName, doFilterGroupsByAppName, alwaysShowCancerStudyGroup, cacheMapUtil);
+ }
+
+ @Bean
+ public MethodSecurityExpressionHandler createExpressionHandler(CancerStudyPermissionEvaluator cancerStudyPermissionEvaluator) {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
- expressionHandler.setPermissionEvaluator(cancerStudyPermissionEvaluator());
+ expressionHandler.setPermissionEvaluator(cancerStudyPermissionEvaluator);
return expressionHandler;
}
-
- @Bean
- public CancerStudyPermissionEvaluator cancerStudyPermissionEvaluator() {
- return new CancerStudyPermissionEvaluator(appName, doFilterGroupsByAppName, alwaysShowCancerStudyGroup, cacheMapUtil);
- }
}
\ No newline at end of file
diff --git a/src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java b/src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java
new file mode 100644
index 00000000000..d9dd8b64bd5
--- /dev/null
+++ b/src/main/java/org/cbioportal/service/exception/AccessForbiddenException.java
@@ -0,0 +1,7 @@
+package org.cbioportal.service.exception;
+
+public class AccessForbiddenException extends RuntimeException {
+ public AccessForbiddenException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java b/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java
index c408f2f2139..af9749ee052 100644
--- a/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java
+++ b/src/main/java/org/cbioportal/service/util/SessionServiceRequestHandler.java
@@ -4,13 +4,22 @@
import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.List;
+import com.mongodb.BasicDBObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
+import org.cbioportal.web.parameter.VirtualStudy;
+import org.cbioportal.web.parameter.VirtualStudyData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@@ -18,6 +27,8 @@
@Component
public class SessionServiceRequestHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(SessionServiceRequestHandler.class);
+
@Value("${session.service.url:}")
private String sessionServiceURL;
@@ -62,4 +73,81 @@ public String getSessionDataJson(SessionType type, String id) throws Exception {
return responseEntity.getBody();
}
+ /**
+ * Gets virtual study by id
+ * @param id - id of the virtual study to read
+ * @return virtual study
+ */
+ public VirtualStudy getVirtualStudyById(String id) {
+ ResponseEntity responseEntity = new RestTemplate()
+ .exchange(sessionServiceURL + "/virtual_study/" + id,
+ HttpMethod.GET,
+ new HttpEntity<>(getHttpHeaders()),
+ VirtualStudy.class);
+ HttpStatusCode statusCode = responseEntity.getStatusCode();
+ VirtualStudy virtualStudy = responseEntity.getBody();
+ if (!statusCode.is2xxSuccessful() || virtualStudy == null) {
+ LOG.error("The downstream server replied with statusCode={} and body={}." +
+ " Replying with the same status code to the client.",
+ statusCode, virtualStudy);
+ throw new IllegalStateException("The downstream server response is not successful");
+ }
+ return responseEntity.getBody();
+ }
+
+ /**
+ * Get list of virtual studies accessible to user
+ * @param username - user for whom get list of virtual studies
+ * @return - list of virtual studies
+ */
+ public List getVirtualStudiesAccessibleToUser(String username) {
+ BasicDBObject basicDBObject = new BasicDBObject();
+ basicDBObject.put("data.users", username);
+ ResponseEntity> responseEntity = new RestTemplate().exchange(
+ sessionServiceURL + "/virtual_study/query/fetch",
+ HttpMethod.POST,
+ new HttpEntity<>(basicDBObject.toString(), getHttpHeaders()),
+ new ParameterizedTypeReference<>() {
+ });
+
+ return responseEntity.getBody();
+ }
+
+ /**
+ * Creates a virtual study out of virtual study definition (aka virtualStudyData)
+ * @param virtualStudyData - definition of virtual study
+ * @return virtual study object with id and the virtualStudyData
+ */
+ public VirtualStudy createVirtualStudy(VirtualStudyData virtualStudyData) {
+ ResponseEntity responseEntity = new RestTemplate().exchange(
+ sessionServiceURL + "/virtual_study",
+ HttpMethod.POST,
+ new HttpEntity<>(virtualStudyData, getHttpHeaders()),
+ new ParameterizedTypeReference<>() {
+ });
+
+ return responseEntity.getBody();
+ }
+
+
+ /**
+ * Soft delete of the virtual study by de-associating all assigned users.
+ * @param id - id of virtual study to soft delete
+ */
+ public void softRemoveVirtualStudy(String id) {
+ VirtualStudy virtualStudy = getVirtualStudyById(id);
+ VirtualStudyData data = virtualStudy.getData();
+ data.setUsers(Collections.emptySet());
+ updateVirtualStudy(virtualStudy);
+ }
+
+ /**
+ * Updates virtual study
+ * @param virtualStudy - virtual study to update
+ */
+ public void updateVirtualStudy(VirtualStudy virtualStudy) {
+ new RestTemplate()
+ .put(sessionServiceURL + "/virtual_study/" + virtualStudy.getId(),
+ new HttpEntity<>(virtualStudy.getData(), getHttpHeaders()));
+ }
}
diff --git a/src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java b/src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java
new file mode 100644
index 00000000000..c48ac446041
--- /dev/null
+++ b/src/main/java/org/cbioportal/web/PublicVirtualStudiesController.java
@@ -0,0 +1,139 @@
+package org.cbioportal.web;
+
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import org.cbioportal.service.CancerTypeService;
+import org.cbioportal.service.exception.AccessForbiddenException;
+import org.cbioportal.service.exception.CancerTypeNotFoundException;
+import org.cbioportal.service.util.SessionServiceRequestHandler;
+import org.cbioportal.web.parameter.VirtualStudy;
+import org.cbioportal.web.parameter.VirtualStudyData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+@Controller
+@RequestMapping("/api/public_virtual_studies")
+public class PublicVirtualStudiesController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PublicVirtualStudiesController.class);
+
+ public static final String ALL_USERS = "*";
+
+ private final String requiredPublisherApiKey;
+
+ private final SessionServiceRequestHandler sessionServiceRequestHandler;
+
+ private final CancerTypeService cancerTypeService;
+
+ public PublicVirtualStudiesController(
+ @Value("${session.endpoint.publisher-api-key:}") String requiredPublisherApiKey,
+ SessionServiceRequestHandler sessionServiceRequestHandler,
+ CancerTypeService cancerTypeService
+ ) {
+ this.requiredPublisherApiKey = requiredPublisherApiKey;
+ this.sessionServiceRequestHandler = sessionServiceRequestHandler;
+ this.cancerTypeService = cancerTypeService;
+ }
+
+ @GetMapping
+ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = VirtualStudy.class)))
+ public ResponseEntity> getPublicVirtualStudies() {
+ List virtualStudies = sessionServiceRequestHandler.getVirtualStudiesAccessibleToUser(ALL_USERS);
+ return new ResponseEntity<>(virtualStudies, HttpStatus.OK);
+ }
+
+ @PostMapping("/{id}")
+ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = VirtualStudy.class)))
+ public ResponseEntity publishVirtualStudy(
+ @PathVariable String id,
+ @RequestHeader(value = "X-PUBLISHER-API-KEY") String providedPublisherApiKey,
+ @RequestParam(required = false) String typeOfCancerId,
+ @RequestParam(required = false) String pmid
+ ) {
+ ensureProvidedPublisherApiKeyCorrect(providedPublisherApiKey);
+ publishVirtualStudy(id, typeOfCancerId, pmid);
+ return ResponseEntity.ok().build();
+ }
+
+ @DeleteMapping("/{id}")
+ @ApiResponse(responseCode = "200", description = "OK")
+ public ResponseEntity unPublishVirtualStudy(
+ @PathVariable String id,
+ @RequestHeader(value = "X-PUBLISHER-API-KEY") String providedPublisherApiKey
+ ) {
+ ensureProvidedPublisherApiKeyCorrect(providedPublisherApiKey);
+ unPublishVirtualStudy(id);
+ return ResponseEntity.ok().build();
+ }
+
+ /**
+ * Publishes virtual study optionally updating metadata fields
+ * @param id - id of public virtual study to publish
+ * @param typeOfCancerId - if specified (not null) update type of cancer of published virtual study
+ * @param pmid - if specified (not null) update PubMed ID of published virtual study
+ */
+ private void publishVirtualStudy(String id, String typeOfCancerId, String pmid) {
+ VirtualStudy virtualStudyDataToPublish = sessionServiceRequestHandler.getVirtualStudyById(id);
+ VirtualStudyData virtualStudyData = virtualStudyDataToPublish.getData();
+ updateStudyMetadataFieldsIfSpecified(virtualStudyData, typeOfCancerId, pmid);
+ virtualStudyData.setUsers(Set.of(ALL_USERS));
+ sessionServiceRequestHandler.updateVirtualStudy(virtualStudyDataToPublish);
+ }
+
+ /**
+ * Un-publish virtual study
+ * @param id - id of public virtual study to un-publish
+ */
+ private void unPublishVirtualStudy(String id) {
+ VirtualStudy virtualStudyToUnPublish = sessionServiceRequestHandler.getVirtualStudyById(id);
+ if (virtualStudyToUnPublish == null) {
+ throw new NoSuchElementException("The virtual study with id=" + id + " has not been found in the public list.");
+ }
+ VirtualStudyData virtualStudyData = virtualStudyToUnPublish.getData();
+ Set users = virtualStudyData.getUsers();
+ if (users == null || users.isEmpty() || !users.contains(ALL_USERS)) {
+ throw new NoSuchElementException("The virtual study with id=" + id + " has not been found in the public list.");
+ }
+ virtualStudyData.setUsers(Set.of(virtualStudyData.getOwner()));
+ sessionServiceRequestHandler.updateVirtualStudy(virtualStudyToUnPublish);
+ }
+
+ private void ensureProvidedPublisherApiKeyCorrect(String providedPublisherApiKey) {
+ if (requiredPublisherApiKey.isBlank()
+ || !requiredPublisherApiKey.equals(providedPublisherApiKey)) {
+ throw new AccessForbiddenException("The provided publisher API key is not correct.");
+ }
+ }
+
+ private void updateStudyMetadataFieldsIfSpecified(VirtualStudyData virtualStudyData, String typeOfCancerId, String pmid) {
+ if (typeOfCancerId != null) {
+ try {
+ cancerTypeService.getCancerType(typeOfCancerId);
+ virtualStudyData.setTypeOfCancerId(typeOfCancerId);
+ } catch (CancerTypeNotFoundException e) {
+ LOG.error("No cancer type with id={} were found.", typeOfCancerId);
+ throw new IllegalArgumentException( "The cancer type is not valid: " + typeOfCancerId);
+ }
+ }
+ if (pmid != null) {
+ virtualStudyData.setPmid(pmid);
+ }
+ }
+
+}
diff --git a/src/main/java/org/cbioportal/web/SessionServiceController.java b/src/main/java/org/cbioportal/web/SessionServiceController.java
index 6a2395cd469..83cf95c932a 100644
--- a/src/main/java/org/cbioportal/web/SessionServiceController.java
+++ b/src/main/java/org/cbioportal/web/SessionServiceController.java
@@ -58,6 +58,8 @@
import java.util.Set;
import java.util.regex.Pattern;
+import static org.cbioportal.web.PublicVirtualStudiesController.ALL_USERS;
+
@Controller
@RequestMapping("/api/session")
public class SessionServiceController {
@@ -132,12 +134,17 @@ private ResponseEntity addSession(
if (type.equals(Session.SessionType.virtual_study) || type.equals(Session.SessionType.group)) {
// JSON from file to Object
VirtualStudyData virtualStudyData = sessionServiceObjectMapper.readValue(body.toString(), VirtualStudyData.class);
+ //TODO sanitize what's supplied. e.g. anonymous user should not specify the users field!
if (isAuthorized()) {
- virtualStudyData.setOwner(userName());
+ String userName = userName();
+ if (userName.equals(ALL_USERS)) {
+ throw new IllegalStateException("Illegal username " + ALL_USERS + " for assigning virtual studies.");
+ }
+ virtualStudyData.setOwner(userName);
if ((operation.isPresent() && operation.get().equals(SessionOperation.save))
|| type.equals(Session.SessionType.group)) {
- virtualStudyData.setUsers(Collections.singleton(userName()));
+ virtualStudyData.setUsers(Collections.singleton(userName));
}
}
@@ -246,7 +253,8 @@ public ResponseEntity> getUserStudies() throws JsonProcessing
httpEntity,
new ParameterizedTypeReference>() {});
- return new ResponseEntity<>(responseEntity.getBody(), HttpStatus.OK);
+ List virtualStudyList = responseEntity.getBody();
+ return new ResponseEntity<>(virtualStudyList, HttpStatus.OK);
} catch (Exception exception) {
LOG.error("Error occurred", exception);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
@@ -260,7 +268,7 @@ public ResponseEntity> getUserStudies() throws JsonProcessing
content = @Content(schema = @Schema(implementation = Session.class)))
public ResponseEntity addSession(@PathVariable Session.SessionType type, @RequestBody JSONObject body)
throws IOException {
-
+ //FIXME? anonymous user can create sessions. Do we really want that? https://github.com/cBioPortal/cbioportal/issues/10843
return addSession(type, Optional.empty(), body);
}
@@ -268,7 +276,7 @@ public ResponseEntity addSession(@PathVariable Session.SessionType type
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = Session.class)))
public ResponseEntity addUserSavedVirtualStudy(@RequestBody JSONObject body) throws IOException {
-
+ //FIXME? anonymous user can create virtual studies. Do we really want that? https://github.com/cBioPortal/cbioportal/issues/10843
return addSession(Session.SessionType.virtual_study, Optional.of(SessionOperation.save), body);
}
diff --git a/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java b/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java
index 75bc5c55e6e..1c24d60c431 100644
--- a/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java
+++ b/src/main/java/org/cbioportal/web/error/GlobalExceptionHandler.java
@@ -18,6 +18,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.Iterator;
+import java.util.NoSuchElementException;
// TODO
@@ -162,6 +163,12 @@ public ResponseEntity handleDataAccessTokenProhibitedUserExceptio
return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
}
+ @ExceptionHandler(AccessForbiddenException.class)
+ public ResponseEntity handleAccessForbiddenException() {
+ ErrorResponse response = new ErrorResponse("The access is forbidden.");
+ return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED);
+ }
+
@ExceptionHandler(TokenNotFoundException.class)
public ResponseEntity handleTokenNotFoundException() {
ErrorResponse response = new ErrorResponse("Specified token cannot be found");
@@ -201,4 +208,9 @@ public ResponseEntity handleBadSqlGrammar(BadSqlGrammarException
HttpStatus.INTERNAL_SERVER_ERROR
);
}
+
+ @ExceptionHandler(NoSuchElementException.class)
+ public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) {
+ return new ResponseEntity<>(new ErrorResponse(ex.getMessage()), HttpStatus.NOT_FOUND);
+ }
}
diff --git a/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java b/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java
index 98cbfa9bd2d..54a5a88a1d6 100644
--- a/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java
+++ b/src/main/java/org/cbioportal/web/parameter/VirtualStudyData.java
@@ -21,6 +21,9 @@ public class VirtualStudyData implements Serializable {
private Long lastUpdated = System.currentTimeMillis();
private Set users = new HashSet<>();
+ private String typeOfCancerId;
+ private String pmid;
+
public String getOwner() {
return owner;
}
@@ -104,4 +107,19 @@ public void setStudyViewFilter(StudyViewFilter studyViewFilter) {
this.studyViewFilter = studyViewFilter;
}
+ public String getTypeOfCancerId() {
+ return typeOfCancerId;
+ }
+
+ public void setTypeOfCancerId(String typeOfCancerId) {
+ this.typeOfCancerId = typeOfCancerId;
+ }
+
+ public String getPmid() {
+ return pmid;
+ }
+
+ public void setPmid(String pmid) {
+ this.pmid = pmid;
+ }
}
diff --git a/src/main/resources/application.properties.EXAMPLE b/src/main/resources/application.properties.EXAMPLE
index 50a440c0bca..82bfe4a07a5 100644
--- a/src/main/resources/application.properties.EXAMPLE
+++ b/src/main/resources/application.properties.EXAMPLE
@@ -239,6 +239,9 @@ session.service.url=https://cbioportal-session-service.herokuapp.com/session_ser
#session.service.user=
#session.service.password=
+# Publishing Virtual Studies
+#session.endpoint.publisher-api-key=
+
# disabled tabs, | delimited
# possible values: cancer_types_summary, mutual_exclusivity, comparison, plots, mutations, co_expression, enrichments, survival, network, download, bookmark, IGV
disabled_tabs=
diff --git a/src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java b/src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java
new file mode 100644
index 00000000000..e2c74996b75
--- /dev/null
+++ b/src/test/java/org/cbioportal/test/integration/PublicVirtualStudiesIntegrationTest.java
@@ -0,0 +1,238 @@
+package org.cbioportal.test.integration;
+
+import org.cbioportal.test.integration.security.ContainerConfig;
+import org.cbioportal.web.parameter.StudyViewFilter;
+import org.cbioportal.web.parameter.VirtualStudy;
+import org.cbioportal.web.parameter.VirtualStudyData;
+import org.cbioportal.web.parameter.VirtualStudySamples;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.cbioportal.test.integration.security.ContainerConfig.MyMysqlInitializer;
+import static org.cbioportal.test.integration.security.ContainerConfig.PortInitializer;
+import static org.cbioportal.test.integration.security.ContainerConfig.SESSION_SERVICE_PORT;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+ webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT
+)
+@TestPropertySource(
+ properties = {
+ "authenticate=false",
+ "session.endpoint.publisher-api-key=this-is-a-secret",
+ "session.service.url=http://localhost:" + SESSION_SERVICE_PORT + "/api/sessions/public_portal/",
+ // DB settings (also see MysqlInitializer)
+ "spring.datasource.driverClassName=com.mysql.jdbc.Driver",
+ "spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect",
+ }
+)
+@ContextConfiguration(initializers = {
+ MyMysqlInitializer.class,
+ PortInitializer.class
+})
+@DirtiesContext
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class PublicVirtualStudiesIntegrationTest extends ContainerConfig {
+
+ static final String CBIO_URL = String.format("http://localhost:%d", CBIO_PORT);
+
+ static final HttpHeaders jsonContentType = new HttpHeaders() {
+ {
+ set("Content-Type", "application/json");
+ }
+ };
+
+ static final HttpHeaders invalidKeyContainingHeaders = new HttpHeaders() {
+ {
+ set("X-PUBLISHER-API-KEY", "this-is-not-valid-key");
+ }
+ };
+
+ static final HttpHeaders validKeyContainingHeaders = new HttpHeaders() {
+ {
+ set("X-PUBLISHER-API-KEY", "this-is-a-secret");
+ }
+ };
+
+ static final ParameterizedTypeReference> typeRef = new ParameterizedTypeReference<>() {
+ };
+
+ static String virtualStudyId;
+
+ static final VirtualStudyData virtualStudyDataToSave = createTestVsData();
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Test
+ public void test1NoPublicVirtualStudiesAtTheBeginning() {
+ ResponseEntity> response1 = restTemplate.exchange(
+ CBIO_URL + "/api/public_virtual_studies",
+ HttpMethod.GET,
+ null,
+ typeRef);
+
+ assertThat(response1.getStatusCode().is2xxSuccessful()).isTrue();
+ assertThat(response1.getBody()).isEmpty();
+ }
+
+ @Test
+ public void test2CreateVirtualStudy() {
+
+ ResponseEntity response2 = restTemplate.exchange(
+ CBIO_URL + "/api/session/virtual_study",
+ HttpMethod.POST,
+ new HttpEntity<>(virtualStudyDataToSave, jsonContentType),
+ VirtualStudy.class);
+ assertThat(response2.getStatusCode().is2xxSuccessful()).isTrue();
+ VirtualStudy savedVs = response2.getBody();
+ assertThat(savedVs).isNotNull().hasFieldOrProperty("id").isNotNull();
+ virtualStudyId = savedVs.getId();
+ }
+
+ @Test
+ public void test2_1UnPublishVirtualStudyFails() {
+ ResponseEntity