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 response = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + new HttpEntity<>(null, validKeyContainingHeaders), + Object.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void test3PublishVirtualStudy() { + String url = CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId + "?typeOfCancerId=acc&pmid=12345"; + ResponseEntity response3 = restTemplate.exchange( + url, + HttpMethod.POST, + null, + Void.class); + assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + + response3 = restTemplate.exchange( + url, + HttpMethod.POST, + new HttpEntity<>(null, invalidKeyContainingHeaders), + Void.class); + assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + + response3 = restTemplate.exchange( + url, + HttpMethod.POST, + new HttpEntity<>(null, validKeyContainingHeaders), + Void.class); + assertThat(response3.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void test4ListJustPublishedStudy() { + ResponseEntity> response4 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies", + HttpMethod.GET, + null, + typeRef); + + assertThat(response4.getStatusCode().is2xxSuccessful()).isTrue(); + List virtualStudies = response4.getBody(); + assertThat(virtualStudies).isNotNull().hasSize(1); + VirtualStudy virtualStudy = virtualStudies.get(0); + VirtualStudyData virtualStudyData = virtualStudy.getData(); + assertThat(virtualStudyData) + .hasFieldOrPropertyWithValue("name", virtualStudyDataToSave.getName()) + .hasFieldOrPropertyWithValue("description", virtualStudyDataToSave.getDescription()) + .hasFieldOrPropertyWithValue("typeOfCancerId", "acc") + .hasFieldOrPropertyWithValue("pmid", "12345"); + assertThat(virtualStudyData.getStudies()).hasSize(2); + assertThat(virtualStudyData.getStudyViewFilter()).isNotNull(); + } + + @Test + public void test5UnpublishVirtualStudy() { + ResponseEntity response5 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + null, + Void.class); + assertThat(response5.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + + response5 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + new HttpEntity<>(null, invalidKeyContainingHeaders), + Void.class); + assertThat(response5.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + + response5 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies/" + virtualStudyId, + HttpMethod.DELETE, + new HttpEntity<>(null, validKeyContainingHeaders), + Void.class); + assertThat(response5.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + public void test6NoPublicVirtualStudiesAfterRemoval() { + ResponseEntity> response6 = restTemplate.exchange( + CBIO_URL + "/api/public_virtual_studies", + HttpMethod.GET, + null, + typeRef); + + assertThat(response6.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response6.getBody()).isEmpty(); + } + + @Test + public void test7UnpublishedVirtualStudyExists() { + ResponseEntity response = restTemplate.exchange( + CBIO_URL + "/api/session/virtual_study/" + virtualStudyId, + HttpMethod.GET, + null, + VirtualStudy.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + VirtualStudy body = response.getBody(); + assertThat(body).isNotNull(); + } + + static VirtualStudyData createTestVsData() { + VirtualStudyData data = new VirtualStudyData(); + data.setName("test virtual study name"); + data.setDescription("test virtual study description"); + VirtualStudySamples study1 = new VirtualStudySamples(); + study1.setId("study_tcga_pub"); + study1.setSamples(Set.of("TCGA-A1-A0SB-01", "TCGA-A1-A0SJ-01")); + VirtualStudySamples study2 = new VirtualStudySamples(); + study2.setId("acc_tcga"); + study2.setSamples(Set.of("TCGA-XX-0800-01")); + Set studies = Set.of( + study1, + study2 + ); + data.setStudies(studies); + StudyViewFilter studyViewFilter = new StudyViewFilter(); + studyViewFilter.setStudyIds(List.of("study_tcga_pub", "acc_tcga")); + data.setStudyViewFilter(studyViewFilter); + return data; + } + +} \ No newline at end of file From f6450f148916332a2bc2813b22187c93a7604fff Mon Sep 17 00:00:00 2001 From: alisman Date: Thu, 11 Jul 2024 11:41:04 -0400 Subject: [PATCH 6/9] Fix sorting in clinicaldata tab of study view (#10889) --- .../ClinicalDataMyBatisRepository.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cbioportal/persistence/mybatis/ClinicalDataMyBatisRepository.java b/src/main/java/org/cbioportal/persistence/mybatis/ClinicalDataMyBatisRepository.java index b9f1e839408..19cdde259fa 100644 --- a/src/main/java/org/cbioportal/persistence/mybatis/ClinicalDataMyBatisRepository.java +++ b/src/main/java/org/cbioportal/persistence/mybatis/ClinicalDataMyBatisRepository.java @@ -148,11 +148,22 @@ public List getVisibleSampleInternalIdsForClinicalTable(List st Integer offset = PaginationCalculator.offset(pageSize, pageNumber); Boolean sortAttrIsNumber = null; Boolean sortIsPatientAttr = null; - if (sortAttrId != null && ! sortAttrId.isEmpty()) { - ClinicalAttribute clinicalAttributeMeta = getClinicalAttributeMeta(studyIds, sortAttrId); - sortAttrIsNumber = clinicalAttributeMeta.getDatatype().equals("NUMBER"); - sortIsPatientAttr = clinicalAttributeMeta.getPatientAttribute(); + + if (sortAttrId != null) { + if (sortAttrId.equals("patientId") || sortAttrId.equals("sampleId")) { + //these are both false because patientId and sampleId are actually not + //clinical attributes and are never numbers + sortAttrIsNumber = false; + sortIsPatientAttr = false; + } else { + ClinicalAttribute clinicalAttributeMeta = getClinicalAttributeMeta(studyIds, sortAttrId); + sortAttrIsNumber = clinicalAttributeMeta.getDatatype().equals("NUMBER"); + sortIsPatientAttr = clinicalAttributeMeta.getPatientAttribute(); + } } + + + return clinicalDataMapper.getVisibleSampleInternalIdsForClinicalTable(studyIds, sampleIds,"SUMMARY", pageSize, offset, searchTerm, sortAttrId, sortAttrIsNumber, sortIsPatientAttr, direction); } From ea8642fdbda2d61d2ab34b9da7a1594680bbbcd5 Mon Sep 17 00:00:00 2001 From: Onur Sumer Date: Fri, 12 Jul 2024 10:34:43 -0400 Subject: [PATCH 7/9] Restrict proxy access to known hosts only (#10884) * restrict proxy access to known hosts only * remove proxy/** endpoint --- .../org/cbioportal/proxy/ProxyController.java | 106 ++---------------- 1 file changed, 10 insertions(+), 96 deletions(-) diff --git a/src/main/java/org/cbioportal/proxy/ProxyController.java b/src/main/java/org/cbioportal/proxy/ProxyController.java index 00ad2e500b4..0296aec3390 100644 --- a/src/main/java/org/cbioportal/proxy/ProxyController.java +++ b/src/main/java/org/cbioportal/proxy/ProxyController.java @@ -13,7 +13,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.security.core.Authentication; -import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -21,16 +20,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.util.Properties; import java.util.regex.Pattern; // TODO Consider creating separate DispatcherServlets as in the original web.xml @@ -64,27 +57,13 @@ public class ProxyController { @Value("${darwin.regex:Test}") private String darwinRegex; - - - @RequestMapping("/**") - public String proxy(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) - throws URISyntaxException { - HttpHeaders httpHeaders = initHeaders(request); - // TODO when reimplemeting different dispatcherservlets with different context roots - // reset this to 'String requestPathInfo = request.getPathInfo();' - String requestPathInfo = request.getPathInfo() == null? request.getServletPath() : request.getPathInfo(); - requestPathInfo = requestPathInfo.replace("proxy/", ""); - return exchangeData(body, - buildUri(requestPathInfo, request.getQueryString(), false), - method, - httpHeaders, - String.class - ).getBody(); - } - //TODO: Hey figure out if we need this - @RequestMapping("/legacy/proxy/oncokb/**") - public String legacyProxyOncokb( + /** + * This dev endpoint can be used (with a personal access token) instead of the production endpoint. + * This is useful when debugging the frontend proxy API calls. + */ + @RequestMapping("/dev/oncokb/**") + public String devProxyOncokb( @RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request @@ -94,7 +73,7 @@ public String legacyProxyOncokb( return exchangeOncokbData( body, - request.getPathInfo().replaceFirst("/oncokb", ""), + request.getPathInfo().replaceFirst("/dev/oncokb", ""), request.getQueryString(), method, getOncokbHeaders(request, token) @@ -168,32 +147,6 @@ private HttpHeaders getOncokbHeaders(HttpServletRequest request, String token) { return httpHeaders; } - //TODO: Figure out what is different (Rebased from Spring Boot Branch) - @RequestMapping("/proxy/oncokb/**") - public String proxyOncokb(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) - throws URISyntaxException { - - if (!this.showOncokb) { - throw new OncoKBServiceIsDisabledException(); - } - - HttpHeaders httpHeaders = initHeaders(request); - - if (!ObjectUtils.isEmpty(this.oncokbToken)) { - httpHeaders.add("Authorization", "Bearer " + this.oncokbToken); - } - - // TODO when reimplemeting different dispatcherservlets with different context roots - // reset this to 'String requestPathInfo = request.getPathInfo();' - String requestPathInfo = request.getPathInfo() == null? request.getServletPath() : request.getPathInfo(); - String replaceString = request.getPathInfo() == null? "/proxy/oncokb" : "/oncokb"; - return exchangeData(body, - buildUri(this.oncokbApiUrl + requestPathInfo.replaceFirst(replaceString, ""), request.getQueryString()), - method, - httpHeaders, - String.class).getBody(); - } - @GetMapping("/checkDarwinAccess") public ResponseEntity checkDarwinAccess(HttpServletRequest request, Authentication authentication) { String user = authentication != null ? authentication.getName(): "anonymousUser"; @@ -226,54 +179,15 @@ private ResponseEntity exchangeData(String body, URI uri, HttpMethod meth return restTemplate.exchange(uri, method, new HttpEntity<>(body, httpHeaders), responseType); } - - private InputStream getResourceStream(String propertiesFileName) - { - String resourceFilename = null; - InputStream resourceFIS = null; - - try { - String home = System.getenv("PORTAL_HOME"); - if (home != null) { - resourceFilename = - home + File.separator + propertiesFileName; - resourceFIS = new FileInputStream(resourceFilename); - } - } catch (FileNotFoundException e) { - } - - if (resourceFIS == null) { - resourceFIS = this.getClass().getClassLoader(). - getResourceAsStream(propertiesFileName); - } - - return resourceFIS; - } - private Properties loadProperties(InputStream resourceInputStream) - { - Properties properties = new Properties(); - - try { - properties.load(resourceInputStream); - resourceInputStream.close(); - } - catch (IOException e) { - System.out.println("Error loading properties file: " + e.getMessage()); - } - - return properties; - } - @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "OncoKB service is disabled") public class OncoKBServiceIsDisabledException extends RuntimeException { } - @ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "No OncoKB access token is provided") - public class NoOncoKBTokenProvidedException extends RuntimeException { - } - @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Fair Usage Agreement is missing") public class OncoKBProxyUserAgreementException extends RuntimeException { } + @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Unknown/Invalid hostname") + public class UnknownHostException extends RuntimeException { + } } \ No newline at end of file From 608fca4d4c33ec473c5a847e57bf0394860c1dd4 Mon Sep 17 00:00:00 2001 From: Gaofei Zhao <15748980+dippindots@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:40:20 -0400 Subject: [PATCH 8/9] Frontend v6.0.12 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 20936fa44d7..c9a52bdac76 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.cbioportal cbioportal - 6.0.12-SNAPSHOT + 6.0.12 cBioPortal for Cancer Genomics @@ -27,7 +27,7 @@ com.github.cbioportal - v6.0.11 + v6.0.12 2.13.1 From 9fbfa193b7ad4d9f60cf8fc4298c33ece17e8d2d Mon Sep 17 00:00:00 2001 From: Gaofei Zhao <15748980+dippindots@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:41:42 -0400 Subject: [PATCH 9/9] Prepare v6.0.13 release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c9a52bdac76..654b4be5c5b 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.cbioportal cbioportal - 6.0.12 + 6.0.13-SNAPSHOT cBioPortal for Cancer Genomics