aglinxinyuan commented on code in PR #7592:
URL: https://github.com/apache/texera/pull/7592#discussion_r3771557105


##########
amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala:
##########
@@ -1041,4 +1060,232 @@ class WorkflowResourceSpec
     assert(names.contains("dup-src") && names.contains("dup-src_copy"))
   }
 
+  // ─── shared-access and failure paths (issue #7224) 
──────────────────────────
+
+  // Content with an operators array: duplicate/clone reassign operator ids 
and fail
+  // on content that has none.
+  private val contentWithOperator =
+    
"{\"operators\":[{\"operatorID\":\"op1\",\"operatorType\":\"CSVFileScan\"}]}"
+
+  private def grantAccess(wid: Integer, user: User, privilege: PrivilegeEnum): 
Unit =
+    new WorkflowUserAccessDao(getDSLContext.configuration())
+      .merge(new WorkflowUserAccess(user.getUid, wid, privilege))
+
+  // A request whose remote address is a valid IPv4 so recordCloneAction 
stores it.
+  private def cloneRequest: HttpServletRequest = {
+    val r = stub[HttpServletRequest]
+    (r.getRemoteAddr _).when().returns("127.0.0.1")
+    r
+  }
+
+  private def workflowNamesOf(user: SessionUser): List[String] =
+    
workflowResource.retrieveWorkflowsBySessionUser(user).map(_.workflow.getName)
+
+  private def versionCount(wid: Integer): Int =
+    getDSLContext.fetchCount(WORKFLOW_VERSION, WORKFLOW_VERSION.WID.eq(wid))
+
+  "WorkflowResource.getWorkflowName (companion)" should "return the stored 
name" in {
+    val wid = seedWorkflow(sessionUser1, "companion-name-wf").workflow.getWid
+    assert(WorkflowResource.getWorkflowName(wid) == "companion-name-wf")
+  }
+
+  it should "throw NotFoundException for a wid that does not exist" in {
+    val wid = seedWorkflow(sessionUser1, 
"companion-missing-wf").workflow.getWid
+    assertThrows[NotFoundException](WorkflowResource.getWorkflowName(wid + 
100000))
+  }
+
+  "WorkflowResource.persistWorkflow" should "reject the guest user" in {
+    val workflow = seedWorkflow(sessionUser1, "guest-wf", "d", 
"{\"a\":1}").workflow
+    workflow.setContent("{\"a\":2}")
+
+    // the message distinguishes the guest rejection from the access-privilege 
one
+    val thrown = intercept[ForbiddenException](
+      workflowResource.persistWorkflow(workflow, new 
SessionUser(GuestAuthFilter.GUEST))
+    )
+    assert(thrown.getMessage.contains("Guest user"))
+    assert(workflowResource.retrieveWorkflow(workflow.getWid, 
sessionUser1).content == "{\"a\":1}")
+  }
+
+  it should "update the workflow in place for its owner and record a version" 
in {
+    val workflow = seedWorkflow(sessionUser1, "persist-owner", "d", 
"{\"a\":1}").workflow
+    val versionsBefore = versionCount(workflow.getWid)
+    workflow.setContent("{\"a\":2}")
+
+    val persisted = workflowResource.persistWorkflow(workflow, sessionUser1)
+
+    assert(persisted.getContent == "{\"a\":2}")
+    assert(workflowResource.retrieveWorkflow(workflow.getWid, 
sessionUser1).content == "{\"a\":2}")
+    assert(versionCount(workflow.getWid) == versionsBefore + 1)
+    // updating must not create a second workflow
+    assert(workflowNamesOf(sessionUser1) == List("persist-owner"))
+  }
+
+  it should "let a non-owner with write access update the workflow and record 
a version" in {
+    val workflow = seedWorkflow(sessionUser1, "persist-writer", "d", 
"{\"a\":1}").workflow
+    grantAccess(workflow.getWid, testUser2, PrivilegeEnum.WRITE)
+    val versionsBefore = versionCount(workflow.getWid)
+    workflow.setContent("{\"a\":3}")
+
+    workflowResource.persistWorkflow(workflow, sessionUser2)
+
+    assert(workflowResource.retrieveWorkflow(workflow.getWid, 
sessionUser1).content == "{\"a\":3}")
+    assert(versionCount(workflow.getWid) == versionsBefore + 1)
+    // Guards rather than pins: both already hold before persistWorkflow is 
called, since
+    // seedWorkflow and grantAccess establish them and the write-access branch 
touches only
+    // WORKFLOW and WORKFLOW_VERSION. They document the intent -- the writer 
updates the owner's
+    // workflow rather than getting a copy of its own -- while the two 
assertions above carry the
+    // actual pin.
+    assert(workflowResource.getOwnerName(workflow.getWid) == testUser.getName)
+    assert(workflowNamesOf(sessionUser2) == List("persist-writer"))
+  }
+
+  it should "reject a non-owner with only read access" in {
+    val workflow = seedWorkflow(sessionUser1, "persist-reader", "d", 
"{\"a\":1}").workflow
+    grantAccess(workflow.getWid, testUser2, PrivilegeEnum.READ)
+    workflow.setContent("{\"a\":9}")
+
+    
assertThrows[ForbiddenException](workflowResource.persistWorkflow(workflow, 
sessionUser2))
+    assert(workflowResource.retrieveWorkflow(workflow.getWid, 
sessionUser1).content == "{\"a\":1}")
+  }
+
+  it should "reject a user with no access to an existing workflow" in {
+    val workflow = seedWorkflow(sessionUser1, "persist-no-access", "d", 
"{\"a\":1}").workflow
+    workflow.setContent("{\"a\":9}")
+
+    
assertThrows[ForbiddenException](workflowResource.persistWorkflow(workflow, 
sessionUser2))
+    assert(workflowResource.retrieveWorkflow(workflow.getWid, 
sessionUser1).content == "{\"a\":1}")
+    // the rejected persist must not silently create a copy owned by the caller
+    assert(workflowNamesOf(sessionUser2).isEmpty)
+  }
+
+  "WorkflowResource.createWorkflow" should "reject a workflow that already 
carries an id" in {
+    val existing = seedWorkflow(sessionUser1, "already-has-id").workflow
+
+    
assertThrows[BadRequestException](workflowResource.createWorkflow(existing, 
sessionUser1))
+    assert(workflowNamesOf(sessionUser1) == List("already-has-id"))
+  }
+
+  "WorkflowResource.updateWorkflowName" should "accept a non-owner with write 
access" in {
+    val wid = seedWorkflow(sessionUser1, "writer-rename").workflow.getWid
+    grantAccess(wid, testUser2, PrivilegeEnum.WRITE)
+    val update = new Workflow()
+    update.setWid(wid)
+    update.setName("renamed-by-writer")
+
+    workflowResource.updateWorkflowName(update, sessionUser2)
+
+    assert(workflowResource.getWorkflowName(wid) == "renamed-by-writer")
+  }
+
+  it should "reject a user with neither ownership nor write access" in {
+    val wid = seedWorkflow(sessionUser1, "no-access-rename").workflow.getWid
+    val update = new Workflow()
+    update.setWid(wid)
+    update.setName("should-not-apply")
+
+    
assertThrows[ForbiddenException](workflowResource.updateWorkflowName(update, 
sessionUser2))
+    assert(workflowResource.getWorkflowName(wid) == "no-access-rename")
+  }
+
+  "WorkflowResource.makePrivate" should "reject a user without write access" 
in {
+    val wid = seedWorkflow(sessionUser1, "private-forbidden").workflow.getWid
+    workflowResource.makePublic(wid, sessionUser1)
+
+    assertThrows[ForbiddenException](workflowResource.makePrivate(wid, 
sessionUser2))
+    assert(workflowResource.getWorkflowType(wid) == "Public")
+  }
+
+  "WorkflowResource.cloneWorkflow" should "copy the workflow to the caller and 
record the clone" in {
+    val wid = seedWorkflow(sessionUser1, "clone-src", "d", 
contentWithOperator).workflow.getWid
+
+    val newWid = workflowResource.cloneWorkflow(wid, sessionUser2, 
cloneRequest)

Review Comment:
   Fixed in c3c0bcafa7 — the guard really is missing, and the test really did 
pin its absence.
   
   `cloneWorkflow` fetches by wid and copies the content with no check. Its 
siblings all guard: `retrieveWorkflow` and `duplicateWorkflow` call 
`hasReadAccess` directly, and `cloneVersion` inherits it through 
`retrieveWorkflowVersion`. This one endpoint was the odd one out, which reads 
as an oversight rather than a decision.
   
   Split across two PRs, since the production half is a behaviour change and 
does not belong in a test-only PR:
   
   - **Here.** The source is published before the clone, which is the flow the 
endpoint actually serves — the hub's clone button, on someone else's published 
workflow. The success case no longer asserts that a caller with no access can 
read a private workflow. `cloneWorkflow` branches on nothing, so the covered 
lines are unchanged, and `!clone.isPublished` gets stronger: with a public 
source it now pins that publicness is not inherited by the clone.
   - **#7605**, stacked on this one, adds the guard along with the rejection 
case you asked for, plus a READ-grant case showing it does not over-block. Red 
before the guard and green after, so the test pins the guard rather than a 
fixture.
   
   Also applied the suppressed comment — the section header now points at #7591 
rather than the earlier #7224.
   



-- 
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