Yicong-Huang commented on code in PR #7994:
URL: https://github.com/apache/texera/pull/7994#discussion_r4058798860


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResource.scala:
##########
@@ -794,6 +795,65 @@ class WorkflowExecutionsResource {
       .entity(Map("error" -> "No sufficient access privilege.").asJava)
       .build()
 
+  /** 403 rather than the 401 above, for a caller who *can* read the workflow 
but may not take
+    * this particular data out of it. The distinction is also operational: the 
frontend's
+    * UnauthorizedHttpInterceptor reads any 401 on an authenticated request as 
an expired
+    * session and logs the user out, which is the wrong outcome for a live 
session that simply
+    * asked for something it is not entitled to.
+    */
+  private def exportDeniedResponse(message: String): Response =
+    Response
+      .status(Response.Status.FORBIDDEN)
+      .`type`(MediaType.APPLICATION_JSON)
+      .entity(Map("error" -> message).asJava)
+      .build()
+
+  /** Authorizes one result-export request, for both the local and the dataset 
endpoint.
+    *
+    * Three things have to hold before results leave the system:
+    *
+    *   1. the caller can read the workflow the results belong to;
+    *   2. the caller can reach the computing unit that produced them — 
`getLatestExecutionID`
+    *      selects on `(wid, cuid)` alone, so the unit is as much a 
caller-chosen key as the
+    *      workflow id is;
+    *   3. no requested operator carries data from a dataset whose owner 
marked it
+    *      non-downloadable. `getWorkflowResultDownloadability` hands the same 
map to the UI,
+    *      which greys those operators out — but that is advice to a 
cooperating client, and
+    *      both export endpoints are reachable directly.
+    *
+    * @return the response to send back, or None when the export may proceed
+    */
+  private def validateUserCanExportResult(
+      user: UserPojo,
+      request: ResultExportRequest
+  ): Option[Response] = {
+    if (!WorkflowAccessResource.hasReadAccess(request.workflowId, 
user.getUid)) {
+      Some(workflowAccessDeniedResponse)
+    } else if (
+      ComputingUnitAccess
+        .getComputingUnitAccess(request.computingUnitId, user.getUid)
+        .eq(PrivilegeEnum.NONE)
+    ) {
+      Some(exportDeniedResponse("No sufficient access privilege to the 
computing unit."))
+    } else {
+      val restrictions = getNonDownloadableOperatorMap(request.workflowId, 
user)

Review Comment:
   **Must fix:**
   
   This map is computed from `WORKFLOW.CONTENT`, the workflow's current JSON 
(`:132-136`). The rows released come from whatever `getLatestExecutionID` 
resolves, and that query joins on `WORKFLOW_VERSION.WID.eq(wid)` (`:103`), so 
it spans every version. The caller controls one input and not the other. Anyone 
with WRITE (the owner always, and the owner is who this control targets) can 
clear the locked scan's `fileName`, save, and re-post the same request. The old 
execution's results still resolve, and the recomputed map no longer names the 
operator. I would compute the map from the executed version, which 
`WorkflowVersionResource.applyPatch` already reconstructs, and make the three 
`Map.empty` returns (`:139`, `:148`, `:160`) deny. If that is too large for 
this PR, say so in the Scaladoc.



##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResource.scala:
##########
@@ -794,6 +795,65 @@ class WorkflowExecutionsResource {
       .entity(Map("error" -> "No sufficient access privilege.").asJava)
       .build()
 
+  /** 403 rather than the 401 above, for a caller who *can* read the workflow 
but may not take

Review Comment:
   **Advisory:**
   
   `UnauthorizedHttpInterceptor` fires only on an `HttpErrorResponse` for a 
request carrying an `Authorization` header 
(`unauthorized-http-interceptor.service.ts:80-88`). That covers 
`/export/dataset`, not `/export/local`: `DownloadService` posts the latter as a 
hidden form into a hidden iframe (`download.service.ts:208-233`), so the 
interceptor never sees the response. On that endpoint the 403 body lands in the 
iframe, so a denied caller gets no feedback and the download simply never 
starts. Fixing that is outside this diff, but narrowing the comment to name the 
dataset endpoint costs one clause.



##########
amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala:
##########
@@ -211,12 +211,14 @@ class WorkflowExecutionsResourceSpec
 
     getDSLContext
       .deleteFrom(WORKFLOW_COMPUTING_UNIT)
-      .where(WORKFLOW_COMPUTING_UNIT.UID.eq(testUserId))
+      .where(WORKFLOW_COMPUTING_UNIT.UID.between(testUserId, testUserId + 10))

Review Comment:
   **Advisory:**
   
   A question, not a retitle demand. `CONTRIBUTING.md`'s type table routes "a 
functionality or a form of support never existed and you are adding it" to 
`feat`, even when the docs already described it as present. Server-side 
enforcement of these two rules never existed, as the description says. Reading 
a security gap as a `fix` is defensible, and the table does not settle it. But 
the prefix is what the auto-label bot keys on: it has already labelled this PR 
`release/v1.2` and queued the backport. Two new 403 refusals headed for a patch 
release deserve a committer's explicit call.



##########
amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala:
##########
@@ -211,12 +211,14 @@ class WorkflowExecutionsResourceSpec
 
     getDSLContext
       .deleteFrom(WORKFLOW_COMPUTING_UNIT)
-      .where(WORKFLOW_COMPUTING_UNIT.UID.eq(testUserId))
+      .where(WORKFLOW_COMPUTING_UNIT.UID.between(testUserId, testUserId + 10))

Review Comment:
   **Must fix:**
   
   The body says "No issue", with no `Closes #N`, no `#N` mention and no 
discussion link, so nothing is linked in the Development sidebar. 
`CONTRIBUTING.md` step 2 asks for an issue and consensus before development. 
Its minor typo and docs exception does not stretch to two new authorization 
refusals on public endpoints, queued for a release backport. Open an issue and 
promote the reference to `Closes #N`. Every other template section is filled, 
including the ASF generative-AI disclosure.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to