Skip to content

Commit

Permalink
Add public datasets under "Hub" (#3129)
Browse files Browse the repository at this point in the history
### Purpose:
Added a new interface for public datasets, allowing non-logged-in users
to view datasets shared by others. The behavior of public datasets
aligns with that of public workflows, ensuring consistent access and
interaction with public resources.

### Changes:
1. Added "Hub Dataset" to the left sidebar. Clicking on it will display
public datasets.
2. Updated the list item behavior: When a user clicks a list item of
type "dataset," different navigation actions are taken depending on the
situation:
- If the user is not logged in, they will always be redirected to the
Hub Dataset page. This page disables the download
functionality and does not display the publish switch.
- If the user is logged in but lacks direct access permissions (e.g.,
read or write), they will still be redirected to the Hub Dataset page.
- If the user is logged in and has read or write permissions, they will
be redirected to the User Dataset page, which is the original dataset
page we had.

### Demos:
**Before:**
Left sidebar:

![image](https://github.com/user-attachments/assets/4d461f79-1a5d-4e65-b6d9-08c55005410d)

**After:**
Left sidebar:

![image](https://github.com/user-attachments/assets/a468cbe7-f40f-40f1-8458-4b5c39ad056a)

Public Dataset Interface Before Login:

![image](https://github.com/user-attachments/assets/89819585-e401-43f6-82bb-8fcb50dace1f)

Public Dataset Interface After Login:

![image](https://github.com/user-attachments/assets/0c0ad66f-764d-4261-9137-0ae433cfd38a)

Non-logged-in user clicking any dataset:

![image](https://github.com/user-attachments/assets/04643d12-57af-4b94-bbb6-331db13ae038)

Logged-in user without direct permissions clicking any dataset:

![image](https://github.com/user-attachments/assets/ce693abc-a6b3-47cc-8b5f-63c0b5e24f62)

Logged-in user with direct permissions clicking any dataset:

![image](https://github.com/user-attachments/assets/df28fd0b-9992-4e40-b480-ec83bfe3dc9d)
  • Loading branch information
GspikeHalo authored Jan 22, 2025
1 parent f319d5a commit 460e078
Show file tree
Hide file tree
Showing 22 changed files with 334 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,4 @@ class DashboardResource {

userIdToInfoMap
}

@GET
@Path("/workflowUserAccess")
def workflowUserAccess(
@QueryParam("wid") wid: UInteger
): util.List[UInteger] = {
val records = context
.select(WORKFLOW_USER_ACCESS.UID)
.from(WORKFLOW_USER_ACCESS)
.where(WORKFLOW_USER_ACCESS.WID.eq(wid))
.fetch()

records.getValues(WORKFLOW_USER_ACCESS.UID)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,18 @@ class HubWorkflowResource {
.where(WORKFLOW_VIEW_COUNT.WID.eq(wid))
.fetchOneInto(classOf[Int])
}

@GET
@Path("/workflowUserAccess")
def workflowUserAccess(
@QueryParam("wid") wid: UInteger
): util.List[UInteger] = {
val records = context
.select(WORKFLOW_USER_ACCESS.UID)
.from(WORKFLOW_USER_ACCESS)
.where(WORKFLOW_USER_ACCESS.WID.eq(wid))
.fetch()

records.getValues(WORKFLOW_USER_ACCESS.UID)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ object DatasetAccessResource {
.getInstance(StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword)
.createDSLContext()

def userHasReadAccess(ctx: DSLContext, did: UInteger, uid: UInteger): Boolean = {
def isDatasetPublic(ctx: DSLContext, did: UInteger): Boolean = {
val datasetDao = new DatasetDao(ctx.configuration())
val isDatasetPublic = Option(datasetDao.fetchOneByDid(did))
Option(datasetDao.fetchOneByDid(did))
.flatMap(dataset => Option(dataset.getIsPublic))
.contains(1.toByte)
}

isDatasetPublic ||
def userHasReadAccess(ctx: DSLContext, did: UInteger, uid: UInteger): Boolean = {
isDatasetPublic(ctx, did) ||
userHasWriteAccess(ctx, did, uid) ||
getDatasetUserAccessPrivilege(ctx, did, uid) == DatasetUserAccessPrivilege.READ
}
Expand Down Expand Up @@ -92,7 +94,7 @@ class DatasetAccessResource {
@GET
@Path("/owner/{did}")
def getOwnerEmailOfDataset(@PathParam("did") did: UInteger): String = {
var email = "";
var email = ""
withTransaction(context) { ctx =>
val owner = getOwner(ctx, did)
if (owner != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,6 @@ object DatasetResource {
}

@Produces(Array(MediaType.APPLICATION_JSON, "image/jpeg", "application/pdf"))
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/dataset")
class DatasetResource {
private val ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE = "User has no read access to this dataset"
Expand All @@ -359,20 +358,27 @@ class DatasetResource {
private def getDashboardDataset(
ctx: DSLContext,
did: UInteger,
uid: UInteger
uid: Option[UInteger],
isPublic: Boolean = false
): DashboardDataset = {
if (!userHasReadAccess(ctx, did, uid)) {
if (
(isPublic && !isDatasetPublic(ctx, did)) ||
(!isPublic && (!userHasReadAccess(ctx, did, uid.get)))
) {
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
}

val targetDataset = getDatasetByID(ctx, did)
val userAccessPrivilege = getDatasetUserAccessPrivilege(ctx, did, uid)
val userAccessPrivilege =
if (isPublic) DatasetUserAccessPrivilege.NONE
else getDatasetUserAccessPrivilege(ctx, did, uid.get)
val isOwner = !isPublic && (targetDataset.getOwnerUid == uid.get)

DashboardDataset(
targetDataset,
getOwner(ctx, did).getEmail,
userAccessPrivilege,
targetDataset.getOwnerUid == uid,
isOwner,
List(),
calculateDatasetVersionSize(did)
)
Expand Down Expand Up @@ -401,6 +407,7 @@ class DatasetResource {
}

@POST
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/create")
@Consumes(Array(MediaType.MULTIPART_FORM_DATA))
def createDataset(
Expand Down Expand Up @@ -477,6 +484,7 @@ class DatasetResource {
}

@POST
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/delete")
def deleteDataset(datasetIDs: DatasetIDs, @Auth user: SessionUser): Response = {
val uid = user.getUid
Expand All @@ -501,6 +509,7 @@ class DatasetResource {
@POST
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/update/name")
def updateDatasetName(
modificator: DatasetNameModification,
Expand All @@ -525,6 +534,7 @@ class DatasetResource {
@POST
@Consumes(Array(MediaType.APPLICATION_JSON))
@Produces(Array(MediaType.APPLICATION_JSON))
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/update/description")
def updateDatasetDescription(
modificator: DatasetDescriptionModification,
Expand All @@ -548,6 +558,7 @@ class DatasetResource {
}

@POST
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/{did}/update/publicity")
def toggleDatasetPublicity(
@PathParam("did") did: UInteger,
Expand All @@ -574,6 +585,7 @@ class DatasetResource {
}

@POST
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/{did}/version/create")
@Consumes(Array(MediaType.MULTIPART_FORM_DATA))
def createDatasetVersion(
Expand Down Expand Up @@ -607,6 +619,7 @@ class DatasetResource {
* @return list of user accessible DashboardDataset objects
*/
@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("")
def listDatasets(
@Auth user: SessionUser
Expand Down Expand Up @@ -684,28 +697,36 @@ class DatasetResource {
}

@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/{did}/version/list")
def getDatasetVersionList(
@PathParam("did") did: UInteger,
@Auth user: SessionUser
): List[DatasetVersion] = {
val uid = user.getUid
withTransaction(context)(ctx => {

if (!userHasReadAccess(ctx, did, uid)) {
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
}
val result: java.util.List[DatasetVersion] = ctx
.selectFrom(DATASET_VERSION)
.where(DATASET_VERSION.DID.eq(did))
.orderBy(DATASET_VERSION.CREATION_TIME.desc()) // or .asc() for ascending
.fetchInto(classOf[DatasetVersion])
fetchDatasetVersions(ctx, did)
})
}

result.asScala.toList
@GET
@Path("/{did}/publicVersion/list")
def getPublicDatasetVersionList(
@PathParam("did") did: UInteger
): List[DatasetVersion] = {
withTransaction(context)(ctx => {
if (!isDatasetPublic(ctx, did)) {
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
}
fetchDatasetVersions(ctx, did)
})
}

@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/{did}/version/latest")
def retrieveLatestDatasetVersion(
@PathParam("did") did: UInteger,
Expand Down Expand Up @@ -753,65 +774,53 @@ class DatasetResource {
}

@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/{did}/version/{dvid}/rootFileNodes")
def retrieveDatasetVersionRootFileNodes(
@PathParam("did") did: UInteger,
@PathParam("dvid") dvid: UInteger,
@Auth user: SessionUser
): DatasetVersionRootFileNodesResponse = {
val uid = user.getUid
withTransaction(context)(ctx =>
fetchDatasetVersionRootFileNodes(ctx, did, dvid, Some(uid), isPublic = false)
)
}

withTransaction(context)(ctx => {
val dataset = getDashboardDataset(ctx, did, uid)
val targetDatasetPath = PathUtils.getDatasetPath(did)
val datasetVersion = getDatasetVersionByID(ctx, dvid)
val datasetName = dataset.dataset.getName
val fileNodes = GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(
targetDatasetPath,
datasetVersion.getVersionHash
)
val versionHash = getDatasetVersionByID(ctx, dvid).getVersionHash
val size = calculateDatasetVersionSize(did, Some(versionHash))
val ownerFileNode = DatasetFileNode
.fromPhysicalFileNodes(
Map((dataset.ownerEmail, datasetName, datasetVersion.getName) -> fileNodes.asScala.toList)
)
.head

DatasetVersionRootFileNodesResponse(
ownerFileNode.children.get
.find(_.getName == datasetName)
.head
.children
.get
.find(_.getName == datasetVersion.getName)
.head
.children
.get,
size
)
})
@GET
@Path("/{did}/publicVersion/{dvid}/rootFileNodes")
def retrievePublicDatasetVersionRootFileNodes(
@PathParam("did") did: UInteger,
@PathParam("dvid") dvid: UInteger
): DatasetVersionRootFileNodesResponse = {
withTransaction(context)(ctx =>
fetchDatasetVersionRootFileNodes(ctx, did, dvid, None, isPublic = true)
)
}

@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/{did}")
def getDataset(
@PathParam("did") did: UInteger,
@Auth user: SessionUser
): DashboardDataset = {
val uid = user.getUid
withTransaction(context)(ctx => {
val dashboardDataset = getDashboardDataset(ctx, did, uid)
val size = calculateDatasetVersionSize(did)
dashboardDataset.copy(size = size)
})
withTransaction(context)(ctx => fetchDataset(ctx, did, Some(uid), isPublic = false))
}

@GET
@Path("/public/{did}")
def getPublicDataset(
@PathParam("did") did: UInteger
): DashboardDataset = {
withTransaction(context)(ctx => fetchDataset(ctx, did, None, isPublic = true))
}

@GET
@Path("/file")
def retrieveDatasetSingleFile(
@QueryParam("path") pathStr: String,
@Auth user: SessionUser
@QueryParam("path") pathStr: String
): Response = {
val decodedPathStr = URLDecoder.decode(pathStr, StandardCharsets.UTF_8.name())

Expand Down Expand Up @@ -863,6 +872,7 @@ class DatasetResource {
* @return A Response containing the dataset version as a ZIP file.
*/
@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@Path("/version-zip")
def retrieveDatasetVersionZip(
@QueryParam("did") did: UInteger,
Expand Down Expand Up @@ -935,4 +945,77 @@ class DatasetResource {
.`type`("application/zip")
.build()
}

@GET
@Path("/datasetUserAccess")
def datasetUserAccess(
@QueryParam("did") did: UInteger
): java.util.List[UInteger] = {
val records = context
.select(DATASET_USER_ACCESS.UID)
.from(DATASET_USER_ACCESS)
.where(DATASET_USER_ACCESS.DID.eq(did))
.fetch()

records.getValues(DATASET_USER_ACCESS.UID)
}

private def fetchDatasetVersions(ctx: DSLContext, did: UInteger): List[DatasetVersion] = {
ctx
.selectFrom(DATASET_VERSION)
.where(DATASET_VERSION.DID.eq(did))
.orderBy(DATASET_VERSION.CREATION_TIME.desc()) // Change to .asc() for ascending order
.fetchInto(classOf[DatasetVersion])
.asScala
.toList
}

private def fetchDatasetVersionRootFileNodes(
ctx: DSLContext,
did: UInteger,
dvid: UInteger,
uid: Option[UInteger],
isPublic: Boolean
): DatasetVersionRootFileNodesResponse = {
val dataset = getDashboardDataset(ctx, did, uid, isPublic)
val targetDatasetPath = PathUtils.getDatasetPath(did)
val datasetVersion = getDatasetVersionByID(ctx, dvid)
val datasetName = dataset.dataset.getName
val fileNodes = GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(
targetDatasetPath,
datasetVersion.getVersionHash
)
val versionHash = datasetVersion.getVersionHash
val size = calculateDatasetVersionSize(did, Some(versionHash))

val ownerFileNode = DatasetFileNode
.fromPhysicalFileNodes(
Map((dataset.ownerEmail, datasetName, datasetVersion.getName) -> fileNodes.asScala.toList)
)
.head

DatasetVersionRootFileNodesResponse(
ownerFileNode.children.get
.find(_.getName == datasetName)
.head
.children
.get
.find(_.getName == datasetVersion.getName)
.head
.children
.get,
size
)
}

private def fetchDataset(
ctx: DSLContext,
did: UInteger,
uid: Option[UInteger],
isPublic: Boolean
): DashboardDataset = {
val dashboardDataset = getDashboardDataset(ctx, did, uid, isPublic)
val size = calculateDatasetVersionSize(did)
dashboardDataset.copy(size = size)
}
}
3 changes: 3 additions & 0 deletions core/gui/src/app/app-routing.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const DASHBOARD_HUB = `${DASHBOARD}/hub`;
export const DASHBOARD_HUB_WORKFLOW = `${DASHBOARD_HUB}/workflow`;
export const DASHBOARD_HUB_WORKFLOW_RESULT = `${DASHBOARD_HUB_WORKFLOW}/result`;
export const DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = `${DASHBOARD_HUB_WORKFLOW_RESULT}/detail`;
export const DASHBOARD_HUB_DATASET = `${DASHBOARD_HUB}/dataset`;
export const DASHBOARD_HUB_DATASET_RESULT = `${DASHBOARD_HUB_DATASET}/result`;
export const DASHBOARD_HUB_DATASET_RESULT_DETAIL = `${DASHBOARD_HUB_DATASET_RESULT}/detail`;

export const DASHBOARD_USER = `${DASHBOARD}/user`;
export const DASHBOARD_USER_PROJECT = `${DASHBOARD_USER}/project`;
Expand Down
Loading

0 comments on commit 460e078

Please sign in to comment.