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

xuang7 pushed a commit to branch release/v1.2
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/release/v1.2 by this push:
     new 0151a665a0 fix(file-service, v1.2): return 403 instead of 500 on 
dataset share/revoke deny paths  (#6994)
0151a665a0 is described below

commit 0151a665a05ef7001448f739fb1232bd869943fa
Author: Yicong Huang <[email protected]>
AuthorDate: Wed Jul 29 14:09:38 2026 -0400

    fix(file-service, v1.2): return 403 instead of 500 on dataset share/revoke 
deny paths  (#6994)
    
    ### What changes were proposed in this PR?
    
    Backport of #6434 to `release/v1.2`, cherry-picked from
    1ff47715057a2c9e3a5ca94761a574f5d6687f63. The cherry-pick applied
    cleanly.
    
    Follows the Direct Backport Push convention; opened as a PR (rather than
    a direct push) per a backport-coverage audit.
    
    ### Any related issues, documentation, discussions?
    
    Backport of #6434. Originally linked #6397, #6427.
    
    ### How was this PR tested?
    
    Release-branch CI runs on this PR. Cherry-pick applied cleanly onto
    `release/v1.2`; no manual conflict resolution was needed.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Yes — backport prepared with Claude Code (mechanical cherry-pick; the
    change itself is #6434 by its original author).
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-authored-by: Eugene Gu <[email protected]>
---
 .../service/resource/DatasetAccessResource.scala   |   2 -
 .../resource/DatasetAccessResourceSpec.scala       | 430 +++++++++++++++++++++
 2 files changed, 430 insertions(+), 2 deletions(-)

diff --git 
a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetAccessResource.scala
 
b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetAccessResource.scala
index 28b90c542a..fd0dce8337 100644
--- 
a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetAccessResource.scala
+++ 
b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetAccessResource.scala
@@ -39,8 +39,6 @@ import 
org.apache.texera.service.resource.DatasetAccessResource.{
 }
 import org.jooq.{DSLContext, EnumType}
 
-import javax.ws.rs.ForbiddenException
-
 object DatasetAccessResource {
   private def context: DSLContext =
     SqlServer
diff --git 
a/file-service/src/test/scala/org/apache/texera/service/resource/DatasetAccessResourceSpec.scala
 
b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetAccessResourceSpec.scala
new file mode 100644
index 0000000000..78fc6f09c5
--- /dev/null
+++ 
b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetAccessResourceSpec.scala
@@ -0,0 +1,430 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.service.resource
+
+import jakarta.ws.rs.ForbiddenException
+import org.apache.texera.auth.SessionUser
+import org.apache.texera.dao.MockTexeraDB
+import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum}
+import 
org.apache.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS
+import org.apache.texera.dao.jooq.generated.tables.daos.{DatasetDao, 
DatasetUserAccessDao, UserDao}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{Dataset, 
DatasetUserAccess, User}
+import org.apache.texera.service.resource.DatasetAccessResource.{
+  getDatasetUserAccessPrivilege,
+  getOwner,
+  isDatasetPublic,
+  userHasReadAccess,
+  userHasWriteAccess,
+  userOwnDataset
+}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+
+import scala.jdk.CollectionConverters._
+
+class DatasetAccessResourceSpec
+    extends AnyFlatSpec
+    with Matchers
+    with MockTexeraDB
+    with BeforeAndAfterAll
+    with BeforeAndAfterEach {
+
+  private val ownerUser: User = {
+    val user = new User
+    user.setName("dataset_owner")
+    user.setPassword("123")
+    user.setEmail("[email protected]")
+    user.setRole(UserRoleEnum.REGULAR)
+    user
+  }
+
+  private val readGranteeUser: User = {
+    val user = new User
+    user.setName("read_grantee")
+    user.setPassword("123")
+    user.setEmail("[email protected]")
+    user.setRole(UserRoleEnum.REGULAR)
+    user
+  }
+
+  private val writeGranteeUser: User = {
+    val user = new User
+    user.setName("write_grantee")
+    user.setPassword("123")
+    user.setEmail("[email protected]")
+    user.setRole(UserRoleEnum.REGULAR)
+    user
+  }
+
+  private val strangerUser: User = {
+    val user = new User
+    user.setName("stranger")
+    user.setPassword("123")
+    user.setEmail("[email protected]")
+    user.setRole(UserRoleEnum.REGULAR)
+    user
+  }
+
+  private val privateDataset: Dataset = {
+    val dataset = new Dataset
+    dataset.setName("private-dataset")
+    dataset.setRepositoryName("private-dataset")
+    dataset.setIsPublic(false)
+    dataset.setIsDownloadable(true)
+    dataset.setDescription("private dataset for access tests")
+    dataset
+  }
+
+  private val publicDataset: Dataset = {
+    val dataset = new Dataset
+    dataset.setName("public-dataset")
+    dataset.setRepositoryName("public-dataset")
+    dataset.setIsPublic(true)
+    dataset.setIsDownloadable(true)
+    dataset.setDescription("public dataset for access tests")
+    dataset
+  }
+
+  private val nonExistentDid: Integer = 999999
+
+  lazy val accessResource = new DatasetAccessResource()
+
+  lazy val ownerSession = new SessionUser(ownerUser)
+  lazy val writeGranteeSession = new SessionUser(writeGranteeUser)
+  lazy val readGranteeSession = new SessionUser(readGranteeUser)
+  lazy val strangerSession = new SessionUser(strangerUser)
+
+  private def grantDirectly(did: Integer, uid: Integer, privilege: 
PrivilegeEnum): Unit = {
+    new DatasetUserAccessDao(getDSLContext.configuration())
+      .insert(new DatasetUserAccess(did, uid, privilege))
+  }
+
+  private def accessList(did: Integer): 
List[DatasetAccessResource.AccessEntry] =
+    accessResource.getAccessList(did).asScala.toList
+
+  override protected def beforeAll(): Unit = {
+    super.beforeAll()
+    initializeDBAndReplaceDSLContext()
+
+    val userDao = new UserDao(getDSLContext.configuration())
+    userDao.insert(ownerUser)
+    userDao.insert(readGranteeUser)
+    userDao.insert(writeGranteeUser)
+    userDao.insert(strangerUser)
+
+    privateDataset.setOwnerUid(ownerUser.getUid)
+    publicDataset.setOwnerUid(ownerUser.getUid)
+    val datasetDao = new DatasetDao(getDSLContext.configuration())
+    datasetDao.insert(privateDataset)
+    datasetDao.insert(publicDataset)
+  }
+
+  override protected def beforeEach(): Unit = {
+    super.beforeEach()
+    // every test starts with no explicit grants
+    getDSLContext.deleteFrom(DATASET_USER_ACCESS).execute()
+  }
+
+  override protected def afterAll(): Unit = {
+    try shutdownDB()
+    finally super.afterAll()
+  }
+
+  // 
===========================================================================
+  // Privilege helpers
+  // 
===========================================================================
+
+  "isDatasetPublic" should "be true for a public dataset and false for a 
private one" in {
+    isDatasetPublic(getDSLContext, publicDataset.getDid) shouldBe true
+    isDatasetPublic(getDSLContext, privateDataset.getDid) shouldBe false
+  }
+
+  "userOwnDataset" should "be true only for the owner" in {
+    userOwnDataset(getDSLContext, privateDataset.getDid, ownerUser.getUid) 
shouldBe true
+    userOwnDataset(getDSLContext, privateDataset.getDid, strangerUser.getUid) 
shouldBe false
+  }
+
+  "getDatasetUserAccessPrivilege" should "return NONE for a user without an 
explicit grant" in {
+    getDatasetUserAccessPrivilege(
+      getDSLContext,
+      privateDataset.getDid,
+      strangerUser.getUid
+    ) shouldEqual PrivilegeEnum.NONE
+  }
+
+  it should "return the granted privilege for a grantee" in {
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+    grantDirectly(privateDataset.getDid, writeGranteeUser.getUid, 
PrivilegeEnum.WRITE)
+
+    getDatasetUserAccessPrivilege(
+      getDSLContext,
+      privateDataset.getDid,
+      readGranteeUser.getUid
+    ) shouldEqual PrivilegeEnum.READ
+    getDatasetUserAccessPrivilege(
+      getDSLContext,
+      privateDataset.getDid,
+      writeGranteeUser.getUid
+    ) shouldEqual PrivilegeEnum.WRITE
+  }
+
+  "the owner" should "have both read and write access to the dataset" in {
+    userHasReadAccess(getDSLContext, privateDataset.getDid, ownerUser.getUid) 
shouldBe true
+    userHasWriteAccess(getDSLContext, privateDataset.getDid, ownerUser.getUid) 
shouldBe true
+  }
+
+  "a READ grantee" should "have read but not write access" in {
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+
+    userHasReadAccess(getDSLContext, privateDataset.getDid, 
readGranteeUser.getUid) shouldBe true
+    userHasWriteAccess(getDSLContext, privateDataset.getDid, 
readGranteeUser.getUid) shouldBe false
+  }
+
+  "a WRITE grantee" should "have both read and write access" in {
+    grantDirectly(privateDataset.getDid, writeGranteeUser.getUid, 
PrivilegeEnum.WRITE)
+
+    userHasReadAccess(getDSLContext, privateDataset.getDid, 
writeGranteeUser.getUid) shouldBe true
+    userHasWriteAccess(getDSLContext, privateDataset.getDid, 
writeGranteeUser.getUid) shouldBe true
+  }
+
+  "a user with no grant" should "have no access to a private dataset" in {
+    userHasReadAccess(getDSLContext, privateDataset.getDid, 
strangerUser.getUid) shouldBe false
+    userHasWriteAccess(getDSLContext, privateDataset.getDid, 
strangerUser.getUid) shouldBe false
+  }
+
+  it should "have read but not write access to a public dataset" in {
+    userHasReadAccess(getDSLContext, publicDataset.getDid, 
strangerUser.getUid) shouldBe true
+    userHasWriteAccess(getDSLContext, publicDataset.getDid, 
strangerUser.getUid) shouldBe false
+  }
+
+  it should "have no explicit privilege row on a public dataset" in {
+    // public read access comes from is_public, not from a dataset_user_access 
row
+    getDatasetUserAccessPrivilege(
+      getDSLContext,
+      publicDataset.getDid,
+      strangerUser.getUid
+    ) shouldEqual PrivilegeEnum.NONE
+  }
+
+  "an explicit WRITE grant on a public dataset" should "give a non-owner write 
access" in {
+    grantDirectly(publicDataset.getDid, writeGranteeUser.getUid, 
PrivilegeEnum.WRITE)
+
+    userHasWriteAccess(getDSLContext, publicDataset.getDid, 
writeGranteeUser.getUid) shouldBe true
+  }
+
+  "the privilege helpers" should "treat a nonexistent dataset as private, 
unowned, and ungranted" in {
+    isDatasetPublic(getDSLContext, nonExistentDid) shouldBe false
+    userOwnDataset(getDSLContext, nonExistentDid, ownerUser.getUid) shouldBe 
false
+    getDatasetUserAccessPrivilege(
+      getDSLContext,
+      nonExistentDid,
+      ownerUser.getUid
+    ) shouldEqual PrivilegeEnum.NONE
+    userHasReadAccess(getDSLContext, nonExistentDid, ownerUser.getUid) 
shouldBe false
+    userHasWriteAccess(getDSLContext, nonExistentDid, ownerUser.getUid) 
shouldBe false
+  }
+
+  "getOwner" should "return the owning user" in {
+    getOwner(getDSLContext, privateDataset.getDid).getEmail shouldEqual 
ownerUser.getEmail
+  }
+
+  it should "return null for a nonexistent dataset" in {
+    getOwner(getDSLContext, nonExistentDid) shouldBe null
+  }
+
+  // 
===========================================================================
+  // grantAccess / getAccessList
+  // 
===========================================================================
+
+  "grantAccess" should "add a grantee that appears in the access list with the 
granted privilege" in {
+    val response = accessResource.grantAccess(
+      privateDataset.getDid,
+      readGranteeUser.getEmail,
+      "READ",
+      ownerSession
+    )
+    response.getStatus shouldEqual 200
+
+    val entries = accessList(privateDataset.getDid)
+    entries should have size 1
+    entries.head.email shouldEqual readGranteeUser.getEmail
+    entries.head.name shouldEqual readGranteeUser.getName
+    entries.head.privilege shouldEqual PrivilegeEnum.READ
+  }
+
+  it should "update the privilege in place when re-granting with a different 
privilege" in {
+    accessResource.grantAccess(
+      privateDataset.getDid,
+      readGranteeUser.getEmail,
+      "READ",
+      ownerSession
+    )
+    accessResource.grantAccess(
+      privateDataset.getDid,
+      readGranteeUser.getEmail,
+      "WRITE",
+      ownerSession
+    )
+
+    val entries = accessList(privateDataset.getDid)
+    entries should have size 1
+    entries.head.email shouldEqual readGranteeUser.getEmail
+    entries.head.privilege shouldEqual PrivilegeEnum.WRITE
+  }
+
+  it should "allow a WRITE grantee to share the dataset" in {
+    grantDirectly(privateDataset.getDid, writeGranteeUser.getUid, 
PrivilegeEnum.WRITE)
+
+    val response = accessResource.grantAccess(
+      privateDataset.getDid,
+      strangerUser.getEmail,
+      "READ",
+      writeGranteeSession
+    )
+    response.getStatus shouldEqual 200
+
+    userHasReadAccess(getDSLContext, privateDataset.getDid, 
strangerUser.getUid) shouldBe true
+  }
+
+  it should "be forbidden for a user without write access" in {
+    val ex = intercept[ForbiddenException] {
+      accessResource.grantAccess(
+        privateDataset.getDid,
+        readGranteeUser.getEmail,
+        "READ",
+        strangerSession
+      )
+    }
+    ex.getResponse.getStatus shouldEqual 403
+    ex.getMessage should include(
+      s"You do not have permission to modify dataset ${privateDataset.getDid}"
+    )
+  }
+
+  it should "be forbidden for a READ grantee" in {
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+
+    assertThrows[ForbiddenException] {
+      accessResource.grantAccess(
+        privateDataset.getDid,
+        strangerUser.getEmail,
+        "READ",
+        readGranteeSession
+      )
+    }
+  }
+
+  "getAccessList" should "return an empty list when no access has been 
granted" in {
+    accessList(privateDataset.getDid) shouldBe empty
+  }
+
+  it should "not include the owner's own access row" in {
+    // even if the owner somehow has an explicit access row, the list only 
shows other users
+    grantDirectly(privateDataset.getDid, ownerUser.getUid, PrivilegeEnum.WRITE)
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+
+    val entries = accessList(privateDataset.getDid)
+    entries should have size 1
+    entries.head.email shouldEqual readGranteeUser.getEmail
+  }
+
+  it should "list multiple grantees with their respective privileges" in {
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+    grantDirectly(privateDataset.getDid, writeGranteeUser.getUid, 
PrivilegeEnum.WRITE)
+
+    val entries = accessList(privateDataset.getDid)
+    entries should have size 2
+    val privilegeByEmail = entries.map(entry => entry.email -> 
entry.privilege).toMap
+    privilegeByEmail(readGranteeUser.getEmail) shouldEqual PrivilegeEnum.READ
+    privilegeByEmail(writeGranteeUser.getEmail) shouldEqual PrivilegeEnum.WRITE
+  }
+
+  // 
===========================================================================
+  // revokeAccess
+  // 
===========================================================================
+
+  "revokeAccess" should "remove the grantee from the access list and drop 
their access" in {
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+
+    val response = accessResource.revokeAccess(
+      privateDataset.getDid,
+      readGranteeUser.getEmail,
+      ownerSession
+    )
+    response.getStatus shouldEqual 200
+
+    accessList(privateDataset.getDid) shouldBe empty
+    getDatasetUserAccessPrivilege(
+      getDSLContext,
+      privateDataset.getDid,
+      readGranteeUser.getUid
+    ) shouldEqual PrivilegeEnum.NONE
+    userHasReadAccess(getDSLContext, privateDataset.getDid, 
readGranteeUser.getUid) shouldBe false
+  }
+
+  it should "allow a WRITE grantee to revoke another user's access" in {
+    grantDirectly(privateDataset.getDid, writeGranteeUser.getUid, 
PrivilegeEnum.WRITE)
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+
+    val response = accessResource.revokeAccess(
+      privateDataset.getDid,
+      readGranteeUser.getEmail,
+      writeGranteeSession
+    )
+    response.getStatus shouldEqual 200
+
+    userHasReadAccess(getDSLContext, privateDataset.getDid, 
readGranteeUser.getUid) shouldBe false
+  }
+
+  it should "succeed as a no-op when the target user has no explicit grant" in 
{
+    val response = accessResource.revokeAccess(
+      privateDataset.getDid,
+      strangerUser.getEmail,
+      ownerSession
+    )
+    response.getStatus shouldEqual 200
+    accessList(privateDataset.getDid) shouldBe empty
+  }
+
+  it should "be forbidden for a user without write access" in {
+    grantDirectly(privateDataset.getDid, readGranteeUser.getUid, 
PrivilegeEnum.READ)
+
+    assertThrows[ForbiddenException] {
+      accessResource.revokeAccess(
+        privateDataset.getDid,
+        readGranteeUser.getEmail,
+        strangerSession
+      )
+    }
+  }
+
+  // 
===========================================================================
+  // getOwnerEmailOfDataset
+  // 
===========================================================================
+
+  "getOwnerEmailOfDataset" should "return the owner's email" in {
+    accessResource.getOwnerEmailOfDataset(privateDataset.getDid) shouldEqual 
ownerUser.getEmail
+  }
+
+  it should "return an empty string for a nonexistent dataset" in {
+    accessResource.getOwnerEmailOfDataset(nonExistentDid) shouldEqual ""
+  }
+}

Reply via email to