This is an automated email from the ASF dual-hosted git repository.

linxinyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/master by this push:
     new 77673b317b fix: private dataset can be set as downloadable (#3666)
77673b317b is described below

commit 77673b317bee0af6606df05ca80452e5d711091c
Author: Seongjin Yoon <[email protected]>
AuthorDate: Mon Aug 18 11:16:06 2025 -0700

    fix: private dataset can be set as downloadable (#3666)
---
 .../texera/service/resource/DatasetResource.scala  | 50 ++++++++++++----------
 .../dataset-detail.component.html                  |  3 +-
 .../dataset-detail.component.scss                  |  3 +-
 .../dataset-detail.component.ts                    | 24 +++++------
 .../user-dataset-version-creator.component.html    |  1 -
 .../user-dataset-version-creator.component.ts      |  5 ---
 .../service/user/download/download.service.spec.ts |  8 ++--
 .../service/user/download/download.service.ts      |  4 +-
 8 files changed, 47 insertions(+), 51 deletions(-)

diff --git 
a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
 
b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
index 3570f05020..a551b79900 100644
--- 
a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
+++ 
b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
@@ -39,8 +39,7 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.daos.{
 import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.{
   Dataset,
   DatasetUserAccess,
-  DatasetVersion,
-  User
+  DatasetVersion
 }
 import edu.uci.ics.texera.service.`type`.DatasetFileNode
 import edu.uci.ics.texera.service.resource.DatasetAccessResource.{
@@ -196,6 +195,7 @@ object DatasetResource {
 class DatasetResource {
   private val ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE = "User has no access 
to this dataset"
   private val ERR_DATASET_VERSION_NOT_FOUND_MESSAGE = "The version of the 
dataset not found"
+  private val EXPIRATION_MINUTES = 5
 
   /**
     * Helper function to get the dataset from DB with additional information 
including user access privilege and owner email
@@ -247,11 +247,6 @@ class DatasetResource {
       val isDatasetPublic = request.isDatasetPublic
       val isDatasetDownloadable = request.isDatasetDownloadable
 
-      // Validate business rule: downloadable can only be true if dataset is 
public
-      if (isDatasetDownloadable && !isDatasetPublic) {
-        throw new BadRequestException("Dataset can only be downloadable if it 
is public")
-      }
-
       // Check if a dataset with the same name already exists
       if (!datasetDao.fetchByName(datasetName).isEmpty) {
         throw new BadRequestException("Dataset with the same name already 
exists")
@@ -573,6 +568,19 @@ class DatasetResource {
     generatePresignedResponse(encodedUrl, datasetName, commitHash, uid)
   }
 
+  @GET
+  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @Path("/presign-download-s3")
+  def getPresignedUrlWithS3(
+      @QueryParam("filePath") encodedUrl: String,
+      @QueryParam("datasetName") datasetName: String,
+      @QueryParam("commitHash") commitHash: String,
+      @Auth user: SessionUser
+  ): Response = {
+    val uid = user.getUid
+    generatePresignedResponse(encodedUrl, datasetName, commitHash, uid)
+  }
+
   @GET
   @Path("/public-presign-download")
   def getPublicPresignedUrl(
@@ -580,9 +588,17 @@ class DatasetResource {
       @QueryParam("datasetName") datasetName: String,
       @QueryParam("commitHash") commitHash: String
   ): Response = {
-    val user = new SessionUser(new User())
-    val uid = user.getUid
-    generatePresignedResponse(encodedUrl, datasetName, commitHash, uid)
+    generatePresignedResponse(encodedUrl, datasetName, commitHash, null)
+  }
+
+  @GET
+  @Path("/public-presign-download-s3")
+  def getPublicPresignedUrlWithS3(
+      @QueryParam("filePath") encodedUrl: String,
+      @QueryParam("datasetName") datasetName: String,
+      @QueryParam("commitHash") commitHash: String
+  ): Response = {
+    generatePresignedResponse(encodedUrl, datasetName, commitHash, null)
   }
 
   @DELETE
@@ -771,11 +787,6 @@ class DatasetResource {
       val newPublicStatus = !existedDataset.getIsPublic
       existedDataset.setIsPublic(newPublicStatus)
 
-      // If dataset becomes private, it must not be downloadable
-      if (!newPublicStatus) {
-        existedDataset.setIsDownloadable(false)
-      }
-
       datasetDao.update(existedDataset)
       Response.ok().build()
     }
@@ -799,11 +810,6 @@ class DatasetResource {
       val existedDataset = getDatasetByID(ctx, did)
       val newDownloadableStatus = !existedDataset.getIsDownloadable
 
-      // Validate business rule: can only set downloadable to true if dataset 
is public
-      if (newDownloadableStatus && !existedDataset.getIsPublic) {
-        throw new BadRequestException("Dataset can only be downloadable if it 
is public")
-      }
-
       existedDataset.setIsDownloadable(newDownloadableStatus)
 
       datasetDao.update(existedDataset)
@@ -1046,8 +1052,8 @@ class DatasetResource {
 
       // Retrieve dataset and check download permission
       val dataset = getDatasetByID(ctx, did)
-      // Non-owners can only download public and downloadable datasets
-      if (!userOwnDataset(ctx, did, uid) && (!dataset.getIsPublic || 
!dataset.getIsDownloadable)) {
+      // Non-owners can download if dataset is downloadable and they have read 
access
+      if (!userOwnDataset(ctx, did, uid) && !dataset.getIsDownloadable) {
         throw new ForbiddenException("Dataset download is not allowed")
       }
 
diff --git 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
index ad982cf99a..348f5daf42 100644
--- 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
+++ 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
@@ -44,11 +44,10 @@
         <div
           class="control-row"
           *ngIf="isOwner">
-          <label>Downloads:</label>
+          <label>Download:</label>
           <nz-switch
             [ngModel]="datasetIsDownloadable"
             (ngModelChange)="onDownloadableStatusChange($event)"
-            [nzDisabled]="!datasetIsPublic"
             nzCheckedChildren="allowed"
             nzUnCheckedChildren="blocked"></nz-switch>
         </div>
diff --git 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
index 3bddc6de15..7e1ba30bdf 100644
--- 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
+++ 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss
@@ -199,6 +199,7 @@ nz-select {
   flex-direction: column;
   gap: 12px;
   margin-right: 20px;
+  margin-bottom: 20px;
 }
 
 .control-row {
@@ -213,7 +214,7 @@ nz-select {
   }
 
   nz-switch {
-    margin-left: auto;
+    margin-left: 8px;
   }
 
   .help-text {
diff --git 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
index f9d6d475b3..2d7d73c017 100644
--- 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
+++ 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
@@ -218,10 +218,6 @@ export class DatasetDetailComponent implements OnInit {
         .subscribe({
           next: (res: Response) => {
             this.datasetIsPublic = checked;
-            // If dataset becomes private, it cannot be downloadable
-            if (!checked) {
-              this.datasetIsDownloadable = false;
-            }
             let state = "public";
             if (!this.datasetIsPublic) {
               state = "private";
@@ -236,12 +232,6 @@ export class DatasetDetailComponent implements OnInit {
   }
 
   onDownloadableStatusChange(checked: boolean): void {
-    // Only allow downloadable change if dataset is public
-    if (checked && !this.datasetIsPublic) {
-      this.notificationService.error("Dataset can only be downloadable if it 
is public");
-      return;
-    }
-
     // Handle the change in dataset downloadable status
     if (this.did) {
       this.datasetService
@@ -307,8 +297,12 @@ export class DatasetDetailComponent implements OnInit {
 
   onClickDownloadCurrentFile = (): void => {
     if (!this.did || !this.selectedVersion?.dvid) return;
-
-    
this.downloadService.downloadSingleFile(this.currentDisplayedFileName).pipe(untilDestroyed(this)).subscribe();
+    // For public datasets accessed by non-owners, use public endpoint
+    const shouldUsePublicEndpoint = this.datasetIsPublic && !this.isOwner;
+    this.downloadService
+      .downloadSingleFile(this.currentDisplayedFileName, 
!shouldUsePublicEndpoint)
+      .pipe(untilDestroyed(this))
+      .subscribe();
   };
 
   onClickScaleTheView() {
@@ -353,8 +347,10 @@ export class DatasetDetailComponent implements OnInit {
     if (this.isOwner) {
       return true;
     }
-    // Non-owners can only download if dataset is public and downloadable
-    return this.datasetIsPublic && this.datasetIsDownloadable;
+    // Non-owners can download if dataset is downloadable and they have access
+    // For public datasets, users have access even if userDatasetAccessLevel 
is 'NONE'
+    // For private datasets, users need explicit access 
(userDatasetAccessLevel !== 'NONE')
+    return this.datasetIsDownloadable && (this.datasetIsPublic || 
this.userDatasetAccessLevel !== "NONE");
   }
 
   // Track multiple file by unique key
diff --git 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.html
 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.html
index cfc530b65a..8320f91772 100644
--- 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.html
+++ 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.html
@@ -45,7 +45,6 @@
           <nz-switch
             [ngModel]="isDatasetDownloadable"
             (ngModelChange)="onDownloadableStatusChange($event)"
-            [nzDisabled]="!isDatasetPublic"
             nzCheckedChildren="enabled"
             nzUnCheckedChildren="disabled"></nz-switch>
         </div>
diff --git 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts
 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts
index f5912d4af1..5e18d198a6 100644
--- 
a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts
+++ 
b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-creator/user-dataset-version-creator.component.ts
@@ -203,11 +203,6 @@ export class UserDatasetVersionCreatorComponent implements 
OnInit {
   onPublicStatusChange(newValue: boolean): void {
     // Handle the change in dataset public status
     this.isDatasetPublic = newValue;
-
-    // If dataset becomes private, disable downloads for security
-    if (!newValue) {
-      this.isDatasetDownloadable = false;
-    }
   }
 
   onDownloadableStatusChange(newValue: boolean): void {
diff --git 
a/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts 
b/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts
index f6d5a4b4b1..3f59997078 100644
--- a/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts
+++ b/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts
@@ -66,11 +66,11 @@ describe("DownloadService", () => {
 
     
datasetServiceSpy.retrieveDatasetVersionSingleFile.and.returnValue(of(mockBlob));
 
-    downloadService.downloadSingleFile(filePath).subscribe({
+    downloadService.downloadSingleFile(filePath, true).subscribe({
       next: blob => {
         expect(blob).toBe(mockBlob);
         expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download file test/file.txt");
-        
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(filePath);
+        
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(filePath,
 true);
         expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(mockBlob, 
"file.txt");
         expect(notificationServiceSpy.success).toHaveBeenCalledWith("File 
test/file.txt has been downloaded");
         done();
@@ -87,14 +87,14 @@ describe("DownloadService", () => {
 
     
datasetServiceSpy.retrieveDatasetVersionSingleFile.and.returnValue(throwError(()
 => new Error(errorMessage)));
 
-    downloadService.downloadSingleFile(filePath).subscribe({
+    downloadService.downloadSingleFile(filePath, true).subscribe({
       next: () => {
         fail("Should have thrown an error");
       },
       error: (error: unknown) => {
         expect(error).toBeTruthy();
         expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to 
download file test/file.txt");
-        
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(filePath);
+        
expect(datasetServiceSpy.retrieveDatasetVersionSingleFile).toHaveBeenCalledWith(filePath,
 true);
         expect(fileSaverServiceSpy.saveAs).not.toHaveBeenCalled();
         expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error 
downloading file 'test/file.txt'");
         done();
diff --git 
a/core/gui/src/app/dashboard/service/user/download/download.service.ts 
b/core/gui/src/app/dashboard/service/user/download/download.service.ts
index 813dea08e2..9c19cf19e2 100644
--- a/core/gui/src/app/dashboard/service/user/download/download.service.ts
+++ b/core/gui/src/app/dashboard/service/user/download/download.service.ts
@@ -93,11 +93,11 @@ export class DownloadService {
     );
   }
 
-  downloadSingleFile(filePath: string): Observable<Blob> {
+  downloadSingleFile(filePath: string, isLogin: boolean = true): 
Observable<Blob> {
     const DEFAULT_FILE_NAME = "download";
     const fileName = filePath.split("/").pop() || DEFAULT_FILE_NAME;
     return this.downloadWithNotification(
-      () => this.datasetService.retrieveDatasetVersionSingleFile(filePath),
+      () => this.datasetService.retrieveDatasetVersionSingleFile(filePath, 
isLogin),
       fileName,
       `Starting to download file ${filePath}`,
       `File ${filePath} has been downloaded`,

Reply via email to