From d8bf88807f308521fe21b15ed5891d4f5d5dc1f2 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Sun, 9 Aug 2026 06:18:04 -0700 Subject: [PATCH] chore(amber): remove the deprecated project feature and its tables --- .../texera/web/TexeraWebApplication.scala | 8 - .../dashboard/DashboardResource.scala | 16 +- .../dashboard/ProjectSearchQueryBuilder.scala | 88 ----- .../dashboard/SearchQueryBuilder.scala | 1 - .../dashboard/UnifiedResourceSchema.scala | 16 +- .../WorkflowSearchQueryBuilder.scala | 23 +- .../resource/dashboard/hub/HubResource.scala | 2 - .../user/project/ProjectAccessResource.scala | 177 --------- .../user/project/ProjectResource.scala | 337 ---------------- .../user/project/PublicProjectResource.scala | 105 ----- .../workflow/WorkflowAccessResource.scala | 13 +- .../user/workflow/WorkflowResource.scala | 45 +-- .../ProjectSearchQueryBuilderSpec.scala | 141 ------- .../dashboard/UnifiedResourceSchemaSpec.scala | 34 +- .../WorkflowSearchQueryBuilderSpec.scala | 55 +-- .../dashboard/file/WorkflowResourceSpec.scala | 116 ++---- .../dashboard/hub/HubResourceSpec.scala | 1 - .../project/ProjectAccessResourceSpec.scala | 265 ------------- .../user/project/ProjectResourceSpec.scala | 367 ------------------ .../project/PublicProjectResourceSpec.scala | 146 ------- common/config/src/main/resources/default.conf | 3 - .../org/apache/texera/dao/MockTexeraDB.scala | 1 - .../service/resource/ConfigResourceSpec.scala | 1 - .../resource/WorkflowAccessResource.scala | 2 +- sql/changelog.xml | 5 + sql/texera_ddl.sql | 53 +-- sql/updates/32.sql | 50 +++ 27 files changed, 116 insertions(+), 1955 deletions(-) delete mode 100644 amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala delete mode 100644 amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala delete mode 100644 amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala delete mode 100644 amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala delete mode 100644 amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala delete mode 100644 amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala delete mode 100644 amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala delete mode 100644 amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala create mode 100644 sql/updates/32.sql diff --git a/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala b/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala index 73e473ba7a4..12c742a1e38 100644 --- a/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala +++ b/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala @@ -39,11 +39,6 @@ import org.apache.texera.web.resource.dashboard.admin.execution.AdminExecutionRe import org.apache.texera.web.resource.dashboard.admin.user.AdminUserResource import org.apache.texera.web.resource.dashboard.hub.HubResource import org.apache.texera.web.resource.dashboard.user.UserResource -import org.apache.texera.web.resource.dashboard.user.project.{ - ProjectAccessResource, - ProjectResource, - PublicProjectResource -} import org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource import org.apache.texera.web.resource.dashboard.user.workflow.{ WorkflowAccessResource, @@ -145,14 +140,11 @@ class TexeraWebApplication environment.jersey.register(classOf[UserConfigResource]) environment.jersey.register(classOf[FeedbackResource]) environment.jersey.register(classOf[AdminUserResource]) - environment.jersey.register(classOf[PublicProjectResource]) environment.jersey.register(classOf[WorkflowAccessResource]) environment.jersey.register(classOf[WorkflowResource]) environment.jersey.register(classOf[HubResource]) environment.jersey.register(classOf[UserResource]) environment.jersey.register(classOf[WorkflowVersionResource]) - environment.jersey.register(classOf[ProjectResource]) - environment.jersey.register(classOf[ProjectAccessResource]) environment.jersey.register(classOf[WorkflowExecutionsResource]) environment.jersey.register(classOf[DashboardResource]) environment.jersey.register(classOf[GmailResource]) diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala index 3d78eef3035..b3808f7cd08 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala @@ -38,7 +38,6 @@ object DashboardResource { case class DashboardClickableFileEntry( resourceType: String, workflow: Option[DashboardWorkflow] = None, - project: Option[Project] = None, dataset: Option[DashboardDataset] = None ) @@ -54,7 +53,7 @@ object DashboardResource { The following class describe the available params from the frontend for full text search. * @param user The authenticated user performing the search. * @param keywords A list of search keywords. The API will return resources that match any of these keywords. - * @param resourceType The type of the resources to include in the search results. Acceptable values are "workflow", "project", "file" and "" (for all types). + * @param resourceType The type of the resources to include in the search results. Acceptable values are "workflow", "file" and "" (for all types). * @param creationStartDate The start of the date range for the creation time filter. It should be provided in 'yyyy-MM-dd' format. * @param creationEndDate The end of the date range for the creation time filter. It should be provided in 'yyyy-MM-dd' format. * @param modifiedStartDate The start of the date range for the modification time filter. It should be provided in 'yyyy-MM-dd' format. @@ -62,7 +61,6 @@ object DashboardResource { * @param owners A list of owner names to include in the search results. * @param workflowIDs A list of workflow IDs to include in the search results. * @param operators A list of operators to include in the search results. - * @param projectIds A list of project IDs to include in the search results. * @param offset The number of initial results to skip. This is useful for implementing pagination. * @param count The maximum number of results to return. * @param orderBy The order in which to sort the results. Acceptable values are 'NameAsc', 'NameDesc', 'CreateTimeDesc', and 'EditTimeDesc'. @@ -77,7 +75,6 @@ object DashboardResource { @QueryParam("owner") owners: java.util.List[String] = new util.ArrayList(), @QueryParam("id") workflowIDs: java.util.List[Integer] = new util.ArrayList(), @QueryParam("operator") operators: java.util.List[String] = new util.ArrayList(), - @QueryParam("projectId") projectIds: java.util.List[Integer] = new util.ArrayList(), @QueryParam("datasetId") datasetIds: java.util.List[Integer] = new util.ArrayList(), @QueryParam("start") @DefaultValue("0") offset: Int = 0, @QueryParam("count") @DefaultValue("20") count: Int = 20, @@ -95,15 +92,12 @@ object DashboardResource { val query = params.resourceType match { case SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE => WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic) - case SearchQueryBuilder.PROJECT_RESOURCE_TYPE => - ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic) case SearchQueryBuilder.DATASET_RESOURCE_TYPE => DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic) case SearchQueryBuilder.ALL_RESOURCE_TYPE => val q1 = WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic) - val q3 = ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic) val q4 = DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic) - q1.unionAll(q3).unionAll(q4) + q1.unionAll(q4) case _ => throw new IllegalArgumentException(s"Unknown resource type: ${params.resourceType}") } @@ -118,8 +112,6 @@ object DashboardResource { resourceType match { case SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE => WorkflowSearchQueryBuilder.toEntry(uid, record) - case SearchQueryBuilder.PROJECT_RESOURCE_TYPE => - ProjectSearchQueryBuilder.toEntry(uid, record) case SearchQueryBuilder.DATASET_RESOURCE_TYPE => DatasetSearchQueryBuilder.toEntry(uid, record) } @@ -180,10 +172,10 @@ object DashboardResource { class DashboardResource { /** - * This method performs a full-text search across all resources - workflows, projects, and files - + * This method performs a full-text search across all resources - workflows and datasets - * that match the specified keywords. * It supports advanced filters such as resource type, creation and modification dates, owner, - * workflow IDs, operators, project IDs and allows to specify the number of results and their ordering. + * workflow IDs and operators, and allows to specify the number of results and their ordering. * * This method utilizes MySQL Boolean Full-Text Searches * reference: https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala deleted file mode 100644 index 5e247f87172..00000000000 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.web.resource.dashboard - -import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS} -import org.apache.texera.dao.jooq.generated.tables.pojos.Project -import org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry -import org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils.{ - getContainsFilter, - getDateFilter, - getFullTextSearchFilter -} -import org.jooq.impl.DSL -import org.jooq.{Condition, GroupField, Record, TableLike} - -import scala.jdk.CollectionConverters.CollectionHasAsScala - -object ProjectSearchQueryBuilder extends SearchQueryBuilder { - - override val mappedResourceSchema: UnifiedResourceSchema = UnifiedResourceSchema( - resourceType = DSL.inline(SearchQueryBuilder.PROJECT_RESOURCE_TYPE), - name = PROJECT.NAME, - description = PROJECT.DESCRIPTION, - creationTime = PROJECT.CREATION_TIME, - lastModifiedTime = PROJECT.CREATION_TIME, - pid = PROJECT.PID, - ownerId = PROJECT.OWNER_ID, - projectColor = PROJECT.COLOR - ) - - override protected def constructFromClause( - uid: Integer, - params: DashboardResource.SearchQueryParams, - includePublic: Boolean = false - ): TableLike[_] = { - PROJECT - .leftJoin(PROJECT_USER_ACCESS) - .on(PROJECT_USER_ACCESS.PID.eq(PROJECT.PID)) - .where(PROJECT_USER_ACCESS.UID.eq(uid)) - } - - override protected def constructWhereClause( - uid: Integer, - params: DashboardResource.SearchQueryParams - ): Condition = { - val splitKeywords = params.keywords.asScala - .flatMap(_.split("[+\\-()<>~*@\"]")) - .filter(_.nonEmpty) - .toSeq - - getDateFilter( - params.creationStartDate, - params.creationEndDate, - PROJECT.CREATION_TIME - ) - .and(getContainsFilter(params.projectIds, PROJECT.PID)) - .and( - getFullTextSearchFilter(splitKeywords, List(PROJECT.NAME, PROJECT.DESCRIPTION)) - ) - } - - override protected def getGroupByFields: Seq[GroupField] = Seq.empty - - override def toEntryImpl( - uid: Integer, - record: Record - ): DashboardResource.DashboardClickableFileEntry = { - val dp = record.into(PROJECT).into(classOf[Project]) - DashboardClickableFileEntry(SearchQueryBuilder.PROJECT_RESOURCE_TYPE, project = Some(dp)) - } -} diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala index e19755e6116..ce2e1cd34d8 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala @@ -35,7 +35,6 @@ object SearchQueryBuilder { .createDSLContext() val FILE_RESOURCE_TYPE = "file" val WORKFLOW_RESOURCE_TYPE = "workflow" - val PROJECT_RESOURCE_TYPE = "project" val DATASET_RESOURCE_TYPE = "dataset" val ALL_RESOURCE_TYPE = "" } diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala index aa3681ae1fb..d1cb9aabe7c 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala @@ -63,13 +63,9 @@ object UnifiedResourceSchema { ownerId: Field[Integer] = DSL.cast(null, classOf[Integer]), wid: Field[Integer] = DSL.cast(null, classOf[Integer]), workflowUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum]), - projectsOfWorkflow: Field[String] = DSL.inline(""), uid: Field[Integer] = DSL.cast(null, classOf[Integer]), userName: Field[String] = DSL.inline(""), userEmail: Field[String] = DSL.inline(""), - pid: Field[Integer] = DSL.cast(null, classOf[Integer]), - projectOwnerId: Field[Integer] = DSL.cast(null, classOf[Integer]), - projectColor: Field[String] = DSL.inline(""), did: Field[Integer] = DSL.cast(null, classOf[Integer]), datasetStoragePath: Field[String] = DSL.cast(null, classOf[String]), repositoryName: Field[String] = DSL.inline(""), @@ -90,13 +86,9 @@ object UnifiedResourceSchema { ownerId -> ownerId.as(resourceOwnerIdAlias), wid -> wid.as("wid"), workflowUserAccess -> workflowUserAccess.as("workflow_privilege"), - projectsOfWorkflow -> projectsOfWorkflow.as("projects"), uid -> uid.as("uid"), userName -> userName.as("userName"), userEmail -> userEmail.as("email"), - pid -> pid.as("pid"), - projectOwnerId -> projectOwnerId.as("owner_uid"), - projectColor -> projectColor.as("color"), did -> did.as("did"), datasetStoragePath -> datasetStoragePath.as("dataset_storage_path"), repositoryName -> repositoryName.as("repository_name"), @@ -114,7 +106,7 @@ object UnifiedResourceSchema { * Refer to /sql/texera_ddl.sql to understand what each attribute is * * Attributes common across all resource types: - * - `resourceType`: The type of the resource (e.g., project, workflow, file) as a `String`. + * - `resourceType`: The type of the resource (e.g., workflow, dataset) as a `String`. * - `name`: The name of the resource as a `String`. * - `description`: A textual description of the resource as a `String`. * - `creationTime`: The timestamp when the resource was created, as a `Timestamp`. @@ -124,16 +116,10 @@ object UnifiedResourceSchema { * Attributes specific to workflows: * - `wid`: Workflow ID, as an `Integer`. * - `workflowUserAccess`: Access privileges associated with the workflow, as a `PrivilegeEnum`. - * - `projectsOfWorkflow`: IDs of projects associated with the workflow, concatenated as a `String`. * - `uid`: User ID associated with the workflow, as an `Integer`. * - `userName`: Name of the user associated with the workflow, as a `String`. * - `userEmail`: Email of the user associated with the workflow, as a `String`. * - * Attributes specific to projects: - * - `pid`: Project ID, as an `Integer`. - * - `projectOwnerId`: ID of the project owner, as an `Integer`. - * - `projectColor`: Color associated with the project, as a `String`. - * * Attributes specific to files: * - `fid`: File ID, as an `Integer`. * - `fileUploadTime`: Timestamp when the file was uploaded, as a `Timestamp`. diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala index b44ccaf30cc..d063dfac585 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala @@ -25,7 +25,6 @@ import org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClick import org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils._ import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow import org.jooq.impl.DSL -import org.jooq.impl.DSL.groupConcatDistinct import org.jooq.{Condition, GroupField, Record, TableLike} import scala.jdk.CollectionConverters.CollectionHasAsScala @@ -53,7 +52,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder { uid = WORKFLOW_OF_USER.UID, ownerId = WORKFLOW_OF_USER.UID, userName = USER.NAME, - projectsOfWorkflow = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID), workflowCoverImage = DSL.max(WORKFLOW_COVER_IMAGE.IMAGE).as("workflow_cover_image") ) } @@ -71,11 +69,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder { .on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID)) .leftJoin(USER) .on(USER.UID.eq(WORKFLOW_OF_USER.UID)) - .leftJoin(WORKFLOW_OF_PROJECT) - .on(WORKFLOW_OF_PROJECT.WID.eq(WORKFLOW.WID)) - .leftJoin(PROJECT_USER_ACCESS) - .on(PROJECT_USER_ACCESS.PID.eq(WORKFLOW_OF_PROJECT.PID)) - .and(if (uid == null) DSL.falseCondition() else PROJECT_USER_ACCESS.UID.eq(uid)) .leftJoin(WORKFLOW_COVER_IMAGE) .on(WORKFLOW_COVER_IMAGE.WID.eq(WORKFLOW.WID)) @@ -83,8 +76,7 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder { if (uid == null) { condition = WORKFLOW.IS_PUBLIC.eq(true) } else { - val privateAccessCondition = - WORKFLOW_USER_ACCESS.UID.eq(uid).or(PROJECT_USER_ACCESS.UID.isNotNull) + val privateAccessCondition = WORKFLOW_USER_ACCESS.UID.eq(uid) if (includePublic) { condition = privateAccessCondition.or(WORKFLOW.IS_PUBLIC.eq(true)) } else { @@ -122,8 +114,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder { .and(getContainsFilter(params.owners, USER.EMAIL)) // Apply operators filter .and(getOperatorsFilter(params.operators, WORKFLOW.CONTENT)) - // Apply projectId filter - .and(getContainsFilter(params.projectIds, WORKFLOW_OF_PROJECT.PID)) // Apply fulltext search filter .and( getFullTextSearchFilter( @@ -150,7 +140,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder { uid: Integer, record: Record ): DashboardResource.DashboardClickableFileEntry = { - val pidField = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID) val dw = DashboardWorkflow( record.into(WORKFLOW_OF_USER).getUid == uid, Option(record.get(WORKFLOW_USER_ACCESS.PRIVILEGE, classOf[PrivilegeEnum])) @@ -158,16 +147,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder { .getOrElse(PrivilegeEnum.NONE.toString), record.into(USER).getName, record.into(WORKFLOW).into(classOf[Workflow]), - if (record.get(pidField) == null) { - List[Integer]() - } else { - record - .get(pidField) - .asInstanceOf[String] - .split(',') - .map(number => Integer.valueOf(number)) - .toList - }, record.into(USER).getUid, Option(record.get("workflow_cover_image", classOf[String])) ) diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala index 33ba16c6e14..09e3bfc8482 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala @@ -494,7 +494,6 @@ class HubResource { DashboardClickableFileEntry( resourceType = entityType.value, workflow = Some(w), - project = None, dataset = None ) } @@ -503,7 +502,6 @@ class HubResource { DashboardClickableFileEntry( resourceType = entityType.value, workflow = None, - project = None, dataset = Some(d) ) } diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala deleted file mode 100644 index a2a7c45715b..00000000000 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.web.resource.dashboard.user.project - -import io.dropwizard.auth.Auth -import org.apache.texera.auth.SessionUser -import org.apache.texera.dao.SqlServer -import org.apache.texera.dao.jooq.generated.Tables.{PROJECT_USER_ACCESS, USER} -import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum -import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectDao, ProjectUserAccessDao, UserDao} -import org.apache.texera.dao.jooq.generated.tables.pojos.ProjectUserAccess -import org.apache.texera.web.model.common.AccessEntry -import org.apache.texera.web.resource.dashboard.user.project.ProjectAccessResource.{ - userHasWriteAccess -} -import org.jooq.DSLContext - -import java.util -import javax.annotation.security.RolesAllowed -import javax.ws.rs._ -import javax.ws.rs.core.MediaType - -object ProjectAccessResource { - private def context: DSLContext = - SqlServer - .getInstance() - .createDSLContext() - - def userHasWriteAccess(pid: Integer, uid: Integer): Boolean = { - getProjectAccessPrivilege(pid, uid) == PrivilegeEnum.WRITE - } - - def getProjectAccessPrivilege(pid: Integer, uid: Integer): PrivilegeEnum = { - Option( - context - .select(PROJECT_USER_ACCESS.PRIVILEGE) - .from(PROJECT_USER_ACCESS) - .where( - PROJECT_USER_ACCESS.PID - .eq(pid) - .and(PROJECT_USER_ACCESS.UID.eq(uid)) - ) - .fetchOneInto(classOf[PrivilegeEnum]) - ).getOrElse(PrivilegeEnum.NONE) - } -} - -@Produces(Array(MediaType.APPLICATION_JSON)) -@RolesAllowed(Array("REGULAR", "ADMIN")) -@Path("/access/project") -class ProjectAccessResource() { - private def context: DSLContext = - SqlServer - .getInstance() - .createDSLContext() - private def userDao = new UserDao(context.configuration()) - private def projectDao = new ProjectDao(context.configuration) - private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration) - - /** - * This method returns the owner of a project - * - * @param pid , project id - * @return ownerEmail, the owner's email - */ - @GET - @Path("/owner/{pid}") - def getOwner(@PathParam("pid") pid: Integer): String = { - userDao.fetchOneByUid(projectDao.fetchOneByPid(pid).getOwnerId).getEmail - } - - /** - * Returns information about all current shared access of the given project - * - * @param pid project id - * @return a List of email/permission pair - */ - @GET - @Path("/list/{pid}") - def getAccessList( - @PathParam("pid") pid: Integer - ): util.List[AccessEntry] = { - context - .select( - USER.EMAIL, - USER.NAME, - PROJECT_USER_ACCESS.PRIVILEGE - ) - .from(PROJECT_USER_ACCESS) - .join(USER) - .on(USER.UID.eq(PROJECT_USER_ACCESS.UID)) - .where( - PROJECT_USER_ACCESS.PID - .eq(pid) - .and(PROJECT_USER_ACCESS.UID.notEqual(projectDao.fetchOneByPid(pid).getOwnerId)) - ) - .fetchInto(classOf[AccessEntry]) - } - - /** - * This method shares a project to a user with a specific access type - * - * @param pid the given project - * @param email the email which the access is given to - * @param privilege the type of Access given to the target user - * @return rejection if user not permitted to share the project or Success Message - */ - @PUT - @Path("/grant/{pid}/{email}/{privilege}") - def grantAccess( - @PathParam("pid") pid: Integer, - @PathParam("email") email: String, - @PathParam("privilege") privilege: String, - @Auth user: SessionUser - ): Unit = { - if (!userHasWriteAccess(pid, user.getUid)) { - throw new ForbiddenException(s"You do not have permission to modify project $pid") - } - - val targetUser = userDao.fetchOneByEmail(email) - if (targetUser == null || targetUser.getIsPlaceholder) { - throw new BadRequestException(s"No registered user with email $email") - } - projectUserAccessDao.merge( - new ProjectUserAccess( - targetUser.getUid, - pid, - PrivilegeEnum.valueOf(privilege) - ) - ) - } - - /** - * Revoke a user's access to a file - * - * @param pid the id of the file - * @param email the email of target user whose access is about to be revoked - * @return A successful resp if granted, failed resp otherwise - */ - @DELETE - @Path("/revoke/{pid}/{email}") - def revokeAccess( - @PathParam("pid") pid: Integer, - @PathParam("email") email: String, - @Auth user: SessionUser - ): Unit = { - if (!userHasWriteAccess(pid, user.getUid)) { - throw new ForbiddenException(s"You do not have permission to modify project $pid") - } - - context - .delete(PROJECT_USER_ACCESS) - .where( - PROJECT_USER_ACCESS.UID - .eq(userDao.fetchOneByEmail(email).getUid) - .and(PROJECT_USER_ACCESS.PID.eq(pid)) - ) - .execute() - } -} diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala deleted file mode 100644 index be72fd21d02..00000000000 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala +++ /dev/null @@ -1,337 +0,0 @@ -/* - * 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.web.resource.dashboard.user.project - -import io.dropwizard.auth.Auth -import org.apache.commons.lang3.StringUtils -import org.apache.texera.auth.SessionUser -import org.apache.texera.dao.SqlServer -import org.apache.texera.dao.jooq.generated.Tables._ -import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum -import org.apache.texera.dao.jooq.generated.tables.daos.{ - ProjectDao, - ProjectUserAccessDao, - WorkflowOfProjectDao -} -import org.apache.texera.dao.jooq.generated.tables.pojos._ -import org.apache.texera.web.resource.dashboard.DashboardResource -import org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams -import org.apache.texera.web.resource.dashboard.user.project.ProjectResource._ -import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess -import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow - -import java.sql.Timestamp -import java.util -import javax.annotation.security.RolesAllowed -import javax.ws.rs._ -import javax.ws.rs.core.MediaType -import scala.jdk.CollectionConverters.IterableHasAsScala - -/** - * This file handles various request related to projects. - * It sends mysql queries to the MysqlDB regarding the 'user_project', - * 'workflow_of_project', and 'file_of_project' Tables - * The details of these tables can be found in /sql/texera_ddl.sql - */ - -object ProjectResource { - private def context = - SqlServer - .getInstance() - .createDSLContext() - private def userProjectDao = new ProjectDao(context.configuration) - private def workflowOfProjectDao = new WorkflowOfProjectDao(context.configuration) - private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration) - - /** - * This method is used to insert any CSV files created from ResultExportService - * handleCSVRequest function into all project(s) that the workflow belongs to. - * - * No insertion occurs if the workflow does not belong to any projects. - * - * @param uid user ID - * @param wid workflow ID - * @param fileName name of exported file - * @return String containing status of adding exported file to project(s) - */ - def addExportedFileToProject(uid: Integer, wid: Integer, fileName: String): String = { - // get map of PIDs and project names - val pidMap = context - .select(WORKFLOW_OF_PROJECT.PID, PROJECT.NAME) - .from(WORKFLOW_OF_PROJECT) - .leftJoin(PROJECT) - .on(WORKFLOW_OF_PROJECT.PID.eq(PROJECT.PID)) - .where(WORKFLOW_OF_PROJECT.WID.eq(wid)) - .fetch() - .intoMap(WORKFLOW_OF_PROJECT.PID, PROJECT.NAME) - - if (pidMap.size() > 0) { // workflow belongs to project(s) - // generate string for ResultExportResponse - if (pidMap.size() == 1) { - s"and added to project: ${pidMap.values().toArray()(0)}" - } else { - s"and added to projects: ${pidMap.values().asScala.mkString(", ")}" - } - } else { // workflow does not belong to a project - "" - } - } - - private def workflowOfProjectExists(wid: Integer, pid: Integer): Boolean = { - workflowOfProjectDao.existsById( - context - .newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID) - .values(wid, pid) - ) - } - - case class DashboardProject( - pid: Integer, - name: String, - description: String, - ownerID: Integer, - creationTime: Timestamp, - color: String, - accessLevel: String - ) -} - -@Path("/project") -@RolesAllowed(Array("REGULAR", "ADMIN")) -@Produces(Array(MediaType.APPLICATION_JSON)) -class ProjectResource { - - /** - * This method returns the specified project - * - * @param pid project id - * @return project specified by the project id - */ - @GET - @Path("/{pid}") - def getProject(@PathParam("pid") pid: Integer): Project = { - userProjectDao.fetchOneByPid(pid) - } - - /** - * This method returns the list of projects owned by the session user. - * - * @param user the session user - * @return a list of projects belonging to owner - */ - @GET - @Path("/list") - def getProjectList(@Auth user: SessionUser): util.List[DashboardProject] = { - context - .selectDistinct( - PROJECT.PID, - PROJECT.NAME, - PROJECT.DESCRIPTION, - PROJECT.OWNER_ID, - PROJECT.CREATION_TIME, - PROJECT.COLOR, - PROJECT_USER_ACCESS.PRIVILEGE - ) - .from(PROJECT_USER_ACCESS) - .join(PROJECT) - .on(PROJECT_USER_ACCESS.PID.eq(PROJECT.PID)) - .where(PROJECT.OWNER_ID.eq(user.getUid).or(PROJECT_USER_ACCESS.UID.eq(user.getUid))) - .fetchInto(classOf[DashboardProject]) - } - - /** - * This method returns a list of DashboardWorkflow objects, which represents - * all the workflows that are part of the specified project. - * - * @param pid project ID - * @param user the session user - * @return list of DashboardWorkflow objects - */ - @GET - @Path("/{pid}/workflows") - def listProjectWorkflows( - @PathParam("pid") pid: Integer, - @Auth user: SessionUser - ): List[DashboardWorkflow] = { - val result = DashboardResource.searchAllResources( - user, - SearchQueryParams(resourceType = "workflow", projectIds = util.Arrays.asList(pid)) - ) - result.results.map(_.workflow.get) - } - - /** - * This method inserts a new project into the database belonging to the session user - * and with the specified name. - * - * @param user the session user - * @param name project name - */ - @POST - @Path("/create/{name}") - def createProject( - @Auth user: SessionUser, - @PathParam("name") name: String - ): Project = { - val project = new Project(null, name, null, user.getUid, null, null) - try { - userProjectDao.insert(project) - projectUserAccessDao.merge( - new ProjectUserAccess(user.getUid, project.getPid, PrivilegeEnum.WRITE) - ) - } catch { - case _: Throwable => - throw new BadRequestException("Cannot create a new project with provided name."); - } - userProjectDao.fetchOneByPid(project.getPid) - } - - /** - * This method adds a mapping between the specified workflow to the specified project into the database. - * - * @param pid project ID - * @param wid workflow ID - */ - @POST - @Path("/{pid}/workflow/{wid}/add") - def addWorkflowToProject( - @PathParam("pid") pid: Integer, - @PathParam("wid") wid: Integer, - @Auth user: SessionUser - ): Unit = { - if (!hasReadAccess(wid, user.getUid)) { - throw new ForbiddenException("No sufficient access privilege to workflow.") - } - - if (!workflowOfProjectExists(wid, pid)) { - workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid)) - } - } - - /** - * This method updates the project name of the specified, existing project - * - * @param pid project ID - * @param name new name - */ - @POST - @Path("/{pid}/rename/{name}") - def updateProjectName( - @PathParam("pid") pid: Integer, - @PathParam("name") name: String - ): Unit = { - val userProject: Project = userProjectDao.fetchOneByPid(pid) - if (StringUtils.isBlank(name)) { - throw new BadRequestException("Cannot rename project to empty or blank name.") - } - - try { - userProject.setName(name) - userProjectDao.update(userProject) - } catch { - case _: Throwable => throw new BadRequestException("Cannot rename project to provided name."); - } - } - - /** - * This method updates the description of a specified, existing project - * - * @param pid project ID - */ - @POST - @Path("/{pid}/update/description") - @Consumes(Array(MediaType.TEXT_PLAIN)) - def updateProjectDescription( - @PathParam("pid") pid: Integer, - description: String - ): Unit = { - val userProject: Project = userProjectDao.fetchOneByPid(pid) - try { - userProject.setDescription(description) - userProjectDao.update(userProject) - } catch { - case _: Throwable => - throw new BadRequestException("Cannot update project description to provided text."); - } - } - - /** - * This method updates a project's color. - * - * @param pid id of project to be updated - * @param colorHex new HEX formatted color to be set - */ - @POST - @Path("/{pid}/color/{colorHex}/add") - def updateProjectColor( - @PathParam("pid") pid: Integer, - @PathParam("colorHex") colorHex: String, - @Auth sessionUser: SessionUser - ): Unit = { - val userProject: Project = userProjectDao.fetchOneByPid(pid) - if ( - colorHex == null || colorHex.length != 6 && colorHex.length != 3 || !colorHex.matches( - "^[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}$" - ) - ) { - throw new BadRequestException("Cannot assign invalid HEX format color to project.") - } - - userProject.setColor(colorHex) - userProjectDao.update(userProject) - } - - @POST - @Path("/{pid}/color/delete") - def deleteProjectColor(@PathParam("pid") pid: Integer): Unit = { - val userProject: Project = userProjectDao.fetchOneByPid(pid) - userProject.setColor(null) - userProjectDao.update(userProject) - } - - /** - * This method deletes an existing project from the database - * - * @param pid projectID - */ - @DELETE - @Path("/delete/{pid}") - def deleteProject(@PathParam("pid") pid: Integer): Unit = { - userProjectDao.deleteById(pid) - } - - /** - * This method deletes an existing mapping between a workflow and project from - * the database - * - * @param pid project ID - * @param wid workflow ID - */ - @DELETE - @Path("/{pid}/workflow/{wid}/delete") - def deleteWorkflowFromProject( - @PathParam("pid") pid: Integer, - @PathParam("wid") wid: Integer - ): Unit = { - workflowOfProjectDao.deleteById( - context.newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID).values(wid, pid) - ) - } -} diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala deleted file mode 100644 index 6983bf13ee0..00000000000 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.web.resource.dashboard.user.project - -import io.dropwizard.auth.Auth -import org.apache.texera.auth.SessionUser -import org.apache.texera.dao.SqlServer -import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PUBLIC_PROJECT, USER} -import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum -import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectUserAccessDao, PublicProjectDao} -import org.apache.texera.dao.jooq.generated.tables.pojos.{ProjectUserAccess, PublicProject} -import org.jooq.DSLContext - -import java.sql.Timestamp -import java.util -import javax.annotation.security.RolesAllowed -import javax.ws.rs._ - -case class DashboardPublicProject( - pid: Integer, - name: String, - owner: String, - creationTime: Timestamp -) {} - -@Path("/public/project") -class PublicProjectResource { - - private def context: DSLContext = - SqlServer - .getInstance() - .createDSLContext() - private def publicProjectDao = new PublicProjectDao(context.configuration) - private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration) - - @GET - @RolesAllowed(Array("ADMIN")) - @Path("/type/{pid}") - def getType(@PathParam("pid") pid: Integer): String = { - if (publicProjectDao.fetchOneByPid(pid) == null) - "Private" - else - "Public" - } - - @PUT - @RolesAllowed(Array("ADMIN")) - @Path("/public/{pid}") - def makePublic(@PathParam("pid") pid: Integer, @Auth user: SessionUser): Unit = { - publicProjectDao.insert(new PublicProject(pid, user.getUid)) - } - - @PUT - @RolesAllowed(Array("ADMIN")) - @Path("/private/{pid}") - def makePrivate(@PathParam("pid") pid: Integer): Unit = { - publicProjectDao.deleteById(pid) - } - - @PUT - @RolesAllowed(Array("REGULAR", "ADMIN")) - @Path("/add") - def addPublicProjects(checkedList: util.List[Integer], @Auth user: SessionUser): Unit = { - checkedList.forEach(pid => { - projectUserAccessDao.merge( - new ProjectUserAccess( - user.getUid, - pid, - PrivilegeEnum.READ - ) - ) - }) - } - - @GET - @RolesAllowed(Array("REGULAR", "ADMIN")) - @Path("/list") - def listPublicProjects(): util.List[DashboardPublicProject] = { - context - .select(PUBLIC_PROJECT.PID, PROJECT.NAME, USER.NAME, PROJECT.CREATION_TIME) - .from(PUBLIC_PROJECT) - .leftJoin(PROJECT) - .on(PUBLIC_PROJECT.PID.eq(PROJECT.PID)) - .leftJoin(USER) - .on(USER.UID.eq(PUBLIC_PROJECT.UID)) - .fetchInto(classOf[DashboardPublicProject]) - } -} diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala index 4fd22f4260c..09353d257f9 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala @@ -86,18 +86,7 @@ object WorkflowAccessResource { .where(WORKFLOW_USER_ACCESS.WID.eq(wid).and(WORKFLOW_USER_ACCESS.UID.eq(uid))) .fetchOneInto(classOf[WorkflowUserAccess]) if (access == null) { - val projectAccess = context - .select() - .from(PROJECT_USER_ACCESS) - .join(WORKFLOW_OF_PROJECT) - .on(WORKFLOW_OF_PROJECT.PID.eq(PROJECT_USER_ACCESS.PID)) - .where(WORKFLOW_OF_PROJECT.WID.eq(wid).and(PROJECT_USER_ACCESS.UID.eq(uid))) - .fetchOneInto(classOf[WorkflowUserAccess]) - if (projectAccess == null) { - PrivilegeEnum.NONE - } else { - projectAccess.getPrivilege - } + PrivilegeEnum.NONE } else { access.getPrivilege } diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala index 72d70d5cf74..ccaf51e2672 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala @@ -31,7 +31,6 @@ import org.apache.texera.dao.jooq.generated.Tables._ import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum import org.apache.texera.dao.jooq.generated.tables.daos.{ WorkflowDao, - WorkflowOfProjectDao, WorkflowOfUserDao, WorkflowUserAccessDao } @@ -39,10 +38,9 @@ import org.apache.texera.dao.jooq.generated.tables.pojos._ import org.apache.texera.service.util.LargeBinaryManager import org.apache.texera.web.resource.dashboard.hub.EntityType import org.apache.texera.web.resource.dashboard.hub.HubResource.recordCloneAction -import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource._ -import org.jooq.impl.DSL.{groupConcatDistinct, noCondition, max} -import org.jooq.{Condition, DSLContext, Record10, Result, SelectOnConditionStep} +import org.jooq.impl.DSL.{noCondition, max} +import org.jooq.{Condition, DSLContext, Record9, Result, SelectOnConditionStep} import java.sql.Timestamp import java.util @@ -75,7 +73,6 @@ object WorkflowResource { new WorkflowUserAccessDao( context.configuration() ) - private def workflowOfProjectDao = new WorkflowOfProjectDao(context.configuration) /** Max length of a stored cover-image data URL. */ private val COVER_IMAGE_MAX_CHARS: Int = 4 * 1024 * 1024 @@ -111,20 +108,11 @@ object WorkflowResource { ) } - private def workflowOfProjectExists(wid: Integer, pid: Integer): Boolean = { - workflowOfProjectDao.existsById( - context - .newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID) - .values(wid, pid) - ) - } - case class DashboardWorkflow( isOwner: Boolean, accessLevel: String, ownerName: String, workflow: Workflow, - projectIDs: List[Integer], ownerId: Integer, coverImage: Option[String] ) @@ -140,7 +128,7 @@ object WorkflowResource { readonly: Boolean ) - case class WorkflowIDs(wids: List[Integer], pid: Option[Integer]) + case class WorkflowIDs(wids: List[Integer]) private def updateWorkflowField( workflow: Workflow, @@ -192,7 +180,7 @@ object WorkflowResource { } } - def baseWorkflowSelect(): SelectOnConditionStep[Record10[ + def baseWorkflowSelect(): SelectOnConditionStep[Record9[ Integer, String, String, @@ -201,7 +189,6 @@ object WorkflowResource { PrivilegeEnum, Integer, String, - String, String ]] = { context @@ -214,7 +201,6 @@ object WorkflowResource { WORKFLOW_USER_ACCESS.PRIVILEGE, WORKFLOW_OF_USER.UID, USER.NAME, - groupConcatDistinct(WORKFLOW_OF_PROJECT.PID).as("projects"), max(WORKFLOW_COVER_IMAGE.IMAGE).as("cover_image") ) .from(WORKFLOW) @@ -224,14 +210,12 @@ object WorkflowResource { .on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID)) .leftJoin(USER) .on(USER.UID.eq(WORKFLOW_OF_USER.UID)) - .leftJoin(WORKFLOW_OF_PROJECT) - .on(WORKFLOW.WID.eq(WORKFLOW_OF_PROJECT.WID)) .leftJoin(WORKFLOW_COVER_IMAGE) .on(WORKFLOW.WID.eq(WORKFLOW_COVER_IMAGE.WID)) } def mapWorkflowEntries( - workflowEntries: Result[Record10[ + workflowEntries: Result[Record9[ Integer, String, String, @@ -240,7 +224,6 @@ object WorkflowResource { PrivilegeEnum, Integer, String, - String, String ]], uid: Integer @@ -258,9 +241,6 @@ object WorkflowResource { .toString, workflowRecord.into(USER).getName, workflowRecord.into(WORKFLOW).into(classOf[Workflow]), - if (workflowRecord.component9() == null) List[Integer]() - else - workflowRecord.component9().split(',').map(str => Integer.valueOf(str)).toList, workflowRecord.into(WORKFLOW_OF_USER).getUid, Option(workflowRecord.get("cover_image", classOf[String])) ) @@ -496,7 +476,6 @@ class WorkflowResource extends LazyLogging { } val resultWorkflows: ListBuffer[DashboardWorkflow] = ListBuffer() - val addToProject = workflowIDs.pid.nonEmpty // then start a transaction and do the duplication try { context.transaction { txConfig => @@ -514,19 +493,6 @@ class WorkflowResource extends LazyLogging { ), sessionUser ) - // if workflows also need to be added to the project - if (addToProject) { - val newWid = newWorkflow.workflow.getWid - if (!hasReadAccess(newWid, user.getUid)) { - throw new ForbiddenException("No sufficient access privilege to workflow.") - } - val pid = workflowIDs.pid.get - if (!workflowOfProjectExists(newWid, pid)) { - workflowOfProjectDao.insert(new WorkflowOfProject(newWid, pid)) - } else { - throw new BadRequestException("Workflow already exists in the project") - } - } resultWorkflows += newWorkflow } } @@ -590,7 +556,6 @@ class WorkflowResource extends LazyLogging { PrivilegeEnum.WRITE.toString, user.getName, workflowDao.fetchOneByWid(workflow.getWid), - List[Integer](), user.getUid, None ) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala deleted file mode 100644 index 0a05846dda1..00000000000 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.web.resource.dashboard - -import org.apache.texera.dao.jooq.generated.Tables.PROJECT -import org.apache.texera.dao.jooq.generated.tables.pojos.Project -import org.jooq.impl.{DSL => JDSL} -import org.jooq.{Record, SQLDialect} - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.sql.Timestamp - -class ProjectSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { - - private val ctx = JDSL.using(SQLDialect.POSTGRES) - - private val ownerUid: Integer = Integer.valueOf(42) - private val callerUid: Integer = Integer.valueOf(43) - private val pid: Integer = Integer.valueOf(7) - private val createdAt = new Timestamp(1700000000123L) - - // In-memory record shaped like the one toEntryImpl receives (keyed by the - // original PROJECT fields). Values are distinct so a wrong-column read fails. - private def translatedRecord( - description: String = "proj-description", - color: String = "aabbcc" - ): Record = { - val record = ctx.newRecord( - PROJECT.PID, - PROJECT.NAME, - PROJECT.DESCRIPTION, - PROJECT.OWNER_ID, - PROJECT.CREATION_TIME, - PROJECT.COLOR - ) - record.set(PROJECT.PID, pid) - record.set(PROJECT.NAME, "proj-name") - record.set(PROJECT.DESCRIPTION, description) - record.set(PROJECT.OWNER_ID, ownerUid) - record.set(PROJECT.CREATION_TIME, createdAt) - record.set(PROJECT.COLOR, color) - record - } - - private def projectOf(record: Record, uid: Integer): Project = - ProjectSearchQueryBuilder.toEntryImpl(uid, record).project.get - - "toEntryImpl" should "copy every project column into the POJO" in { - val p = projectOf(translatedRecord(), ownerUid) - p.getPid shouldBe pid - p.getName shouldBe "proj-name" - p.getDescription shouldBe "proj-description" - p.getOwnerId shouldBe ownerUid - p.getCreationTime shouldBe createdAt - p.getColor shouldBe "aabbcc" - } - - it should "pass a NULL description and a NULL color through as null" in { - // description and color are the only nullable project columns. - val p = projectOf(translatedRecord(description = null, color = null), ownerUid) - p.getDescription shouldBe null - p.getColor shouldBe null - p.getPid shouldBe pid - } - - it should "tag the entry as a project and leave the other payload slots empty" in { - // searchAllResources matches on resourceType with no default branch, so a - // wrong tag is a runtime MatchError. - val entry = ProjectSearchQueryBuilder.toEntryImpl(ownerUid, translatedRecord()) - entry.resourceType shouldBe "project" - entry.project should not be None - entry.workflow shouldBe None - entry.dataset shouldBe None - } - - it should "produce the same entry regardless of the caller's uid" in { - // Unlike the workflow arm there is no ownership flag to compute. - val record = translatedRecord() - ProjectSearchQueryBuilder.toEntryImpl(ownerUid, record) shouldBe - ProjectSearchQueryBuilder.toEntryImpl(callerUid, record) - } - - // An aliased field renders as the bare alias on its own, so render a SELECT. - private lazy val renderedSchema: String = ctx.renderInlined( - JDSL.select(ProjectSearchQueryBuilder.mappedResourceSchema.allFields: _*) - ) - - "mappedResourceSchema" should "project the literal 'project' as the resourceType column" in { - renderedSchema should include("'project' as \"resourceType\"") - } - - it should "alias PROJECT.CREATION_TIME as both the creation and last-modified time" in { - // Deliberate: the project table has no last-modified column. Without this - // alias projects would NULL-sink in every sort by edit time. - val creationTime = ctx.renderInlined(PROJECT.CREATION_TIME) - renderedSchema should include(s"""$creationTime as "resourceCreationTime"""") - renderedSchema should include(s"""$creationTime as "resourceLastModifiedTime"""") - } - - it should "project PROJECT.COLOR as the color column" in { - // The frontend reads the project colour swatch from this alias. - val color = ctx.renderInlined(PROJECT.COLOR) - renderedSchema should include(s"""$color as "color"""") - } - - it should "project the project id and owner through the shared slots" in { - // The owner rides the generic resourceOwnerId slot; the project-specific - // "owner_uid" slot stays NULL and is deliberately not pinned here. - renderedSchema should include(s"""${ctx.renderInlined(PROJECT.PID)} as "pid"""") - renderedSchema should include( - s"""${ctx.renderInlined(PROJECT.OWNER_ID)} as "resourceOwnerId"""" - ) - } - - it should "project the name and description through the shared full-text slots" in { - // Also the two columns the keyword filter targets. - renderedSchema should include(s"""${ctx.renderInlined(PROJECT.NAME)} as "resourceName"""") - renderedSchema should include( - s"""${ctx.renderInlined(PROJECT.DESCRIPTION)} as "resourceDescription"""" - ) - } -} diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala index 9d5f1e0b0bd..63ab264edb7 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala @@ -77,11 +77,10 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { private def translatedOriginals(schema: UnifiedResourceSchema): Seq[Field[_]] = translatedPairs(schema).map(_._1) - // Sentinels for the three slots that have no convenient distinct table + // Sentinels for the two slots that have no convenient distinct table // column of the right type; every other slot uses a real generated column so - // that all 24 originals render differently from one another. + // that all 20 originals render differently from one another. private val sentinelResourceType: Field[String] = JDSL.inline("s-resource-type") - private val sentinelProjects: Field[String] = JDSL.inline("s-projects") private val sentinelStoragePath: Field[String] = JDSL.inline("s-storage-path") private val sentinelSchema: UnifiedResourceSchema = UnifiedResourceSchema( @@ -94,13 +93,9 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { ownerId = WORKFLOW_OF_USER.UID, wid = WORKFLOW.WID, workflowUserAccess = WORKFLOW_USER_ACCESS.PRIVILEGE, - projectsOfWorkflow = sentinelProjects, uid = USER.UID, userName = USER.NAME, userEmail = USER.EMAIL, - pid = PROJECT.PID, - projectOwnerId = PROJECT.OWNER_ID, - projectColor = PROJECT.COLOR, did = DATASET.DID, datasetStoragePath = sentinelStoragePath, repositoryName = DATASET.REPOSITORY_NAME, @@ -122,13 +117,9 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { "resourceOwnerId" -> WORKFLOW_OF_USER.UID, "wid" -> WORKFLOW.WID, "workflow_privilege" -> WORKFLOW_USER_ACCESS.PRIVILEGE, - "projects" -> sentinelProjects, "uid" -> USER.UID, "userName" -> USER.NAME, "email" -> USER.EMAIL, - "pid" -> PROJECT.PID, - "owner_uid" -> PROJECT.OWNER_ID, - "color" -> PROJECT.COLOR, "did" -> DATASET.DID, "dataset_storage_path" -> sentinelStoragePath, "repository_name" -> DATASET.REPOSITORY_NAME, @@ -141,8 +132,8 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { // -- apply(): the projection ------------------------------------------------ - "apply" should "expose all 24 slots as aliases, in the order the UNION ALL depends on" in { - sentinelSchema.allFields should have size 24 + "apply" should "expose all 20 slots as aliases, in the order the UNION ALL depends on" in { + sentinelSchema.allFields should have size 20 sentinelSchema.allFields.map(_.getName) shouldBe expectedProjection.map(_._1) } @@ -162,7 +153,7 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { // about datasets still union with one that does: the column count and // types have to line up. val defaults = UnifiedResourceSchema() - defaults.allFields should have size 24 + defaults.allFields should have size 20 val rendered = ctx.renderInlined(JDSL.select(defaults.allFields: _*)) rendered should include("'' as \"resourceType\"") rendered should include("cast(null as timestamp) as \"resourceCreationTime\"") @@ -197,12 +188,12 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { } it should "collapse the all-defaults projection down to one alias per distinct default" in { - // 24 slots, but only six structurally distinct default expressions, so the + // 20 slots, but only six structurally distinct default expressions, so the // de-dup collapses the map to six entries. Worth pinning because it is // surprising, and because it is what makes the keep-first rule observable at - // all: allFields stays at 24 while the translation map does not. + // all: allFields stays at 20 while the translation map does not. val defaults = UnifiedResourceSchema() - defaults.allFields should have size 24 + defaults.allFields should have size 20 translatedAliases(defaults) shouldBe Seq( "resourceType", // DSL.inline("") "resourceCreationTime", // cast(null as timestamp) @@ -213,7 +204,7 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { ) } - it should "keep every distinct original when the caller supplies 24 distinct Fields" in { + it should "keep every distinct original when the caller supplies 20 distinct Fields" in { // Nothing to collapse here, which is the control case for the two tests // above: the shrinkage they observe comes from duplicate originals only. translatedAliases(sentinelSchema) shouldBe expectedProjection.map(_._1) @@ -221,15 +212,12 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { it should "drop exactly the duplicated slots of the production workflow projection" in { val workflowSchema = WorkflowSearchQueryBuilder.mappedResourceSchema - workflowSchema.allFields should have size 24 + workflowSchema.allFields should have size 20 val aliases = translatedAliases(workflowSchema) // `uid` duplicates ownerId (WORKFLOW_OF_USER.UID); the rest are slots the // builder left at their default, and the defaults collide by type. workflowSchema.allFields.map(_.getName).diff(aliases) shouldBe Seq( "uid", - "owner_uid", - "color", - "did", "repository_name", "is_dataset_downloadable", "cover_image" @@ -241,7 +229,7 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers { "jOOQ Field equality" should "be structural, which is what makes the de-dup collapse anything" in { // If jOOQ ever switched to identity equality, translatedFieldSet would keep - // all 24 slots and translateRecord would start reading duplicated columns — + // all 20 slots and translateRecord would start reading duplicated columns — // the tests above would flip, and this one says why. JDSL.cast(null, classOf[Integer]) shouldBe JDSL.cast(null, classOf[Integer]) JDSL.inline("") shouldBe JDSL.inline("") diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala index 59dd8d9e19f..240e581165d 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala @@ -35,11 +35,6 @@ import org.scalatest.matchers.should.Matchers * which is what makes them reachable from a spec at all. * * Breakage this catches: - * - the `projects` aggregate lookup breaking (a Field looked up by structural - * equality): `record.get(pidField)` would start returning null and every - * workflow would silently report zero projects instead of failing; - * - the comma-split branch losing/reordering ids, or the NULL branch no - * longer yielding an empty list; * - a NULL `workflow_user_access.privilege` (a left join miss for public * workflows) no longer degrading to NONE — an NPE or a wrong grant; * - the ownership flag comparing the wrong uid, or owner id/name being read off @@ -63,10 +58,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { // jOOQ render context (Postgres dialect to match production renderers). private val ctx = JDSL.using(SQLDialect.POSTGRES) - // toEntryImpl re-creates this aggregate locally and then looks it up with - // `record.get(pidField)`; the lookup only resolves because jOOQ compares - // QueryParts structurally. The first test pins that assumption. - private val pidField = JDSL.groupConcatDistinct(WORKFLOW_OF_PROJECT.PID) private val coverField = JDSL.max(WORKFLOW_COVER_IMAGE.IMAGE).as("workflow_cover_image") private val ownerUid: Integer = Integer.valueOf(42) @@ -84,7 +75,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { private def translatedRecord( uidValue: Integer = ownerUid, privilege: PrivilegeEnum = PrivilegeEnum.WRITE, - projects: String = "3,1,2", cover: String = "cover-b64" ): Record = { val record = ctx.newRecord( @@ -94,7 +84,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { WORKFLOW_OF_USER.UID, WORKFLOW_USER_ACCESS.PRIVILEGE, USER.NAME, - pidField, coverField ) record.set(WORKFLOW.WID, wid) @@ -103,7 +92,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { record.set(WORKFLOW_OF_USER.UID, uidValue) record.set(WORKFLOW_USER_ACCESS.PRIVILEGE, privilege) record.set(USER.NAME, "owner-name") - record.set(pidField, projects) record.set(coverField, cover) record } @@ -111,46 +99,9 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { private def workflowOf(record: Record, uid: Integer): DashboardWorkflow = WorkflowSearchQueryBuilder.toEntryImpl(uid, record).workflow.get - // -- the aggregate-field lookup -------------------------------------------- - - "toEntryImpl" should "resolve the project aggregate through structural Field equality" in { - // toEntryImpl builds a *fresh* groupConcatDistinct instance rather than - // reusing the one in mappedResourceSchema, so the whole projects feature - // rides on jOOQ treating two structurally identical aggregates as equal. - val record = translatedRecord(projects = "5,6") - JDSL.groupConcatDistinct(WORKFLOW_OF_PROJECT.PID) shouldBe pidField - record.get(JDSL.groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)) shouldBe "5,6" - } - - // -- projectsOfWorkflow: both branches ------------------------------------- - - it should "split the comma-joined aggregate into Integers, preserving the aggregate's order" in { - // Deliberately unsorted so an accidental `.sorted` / `.reverse` fails. - workflowOf(translatedRecord(projects = "3,1,2"), ownerUid).projectIDs shouldBe - List(Integer.valueOf(3), Integer.valueOf(1), Integer.valueOf(2)) - } - - it should "handle a single-project aggregate (no separator present)" in { - workflowOf(translatedRecord(projects = "8"), ownerUid).projectIDs shouldBe - List(Integer.valueOf(8)) - } - - it should "return an empty project list when the aggregate is NULL" in { - // A workflow that belongs to no project left-joins to a NULL aggregate. - workflowOf(translatedRecord(projects = null), ownerUid).projectIDs shouldBe empty - } - - // Deliberately NOT asserted: that a padded separator ("1, 2") or a leading comma - // (",1") throws NumberFormatException. It does today — `Integer.valueOf` is applied - // straight to the raw `split(',')` output, so such input aborts the whole search - // request with a 500 instead of degrading. But production cannot produce it - // (Postgres' string_agg is rendered with a bare ',' separator), and pinning the - // throw would turn this suite red the moment someone hardens the parser with - // `.map(_.trim).filter(_.nonEmpty)` — i.e. it would punish an improvement. - // -- privilege fallback ----------------------------------------------------- - it should "fall back to NONE when the workflow privilege is NULL" in { + "toEntryImpl" should "fall back to NONE when the workflow privilege is NULL" in { // Public workflows the caller has no explicit grant on left-join to a NULL // privilege; the DTO must still carry a usable access level. workflowOf(translatedRecord(privilege = null), ownerUid).accessLevel shouldBe @@ -204,7 +155,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { it should "tag the entry as a workflow and leave the other payload slots empty" in { val entry = WorkflowSearchQueryBuilder.toEntryImpl(ownerUid, translatedRecord()) entry.resourceType shouldBe "workflow" - entry.project shouldBe None entry.dataset shouldBe None entry.workflow should not be None } @@ -221,8 +171,5 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers { JDSL.select(WorkflowSearchQueryBuilder.mappedResourceSchema.allFields: _*) ) rendered should include("'workflow' as \"resourceType\"") - // The projects column is the aggregate toEntryImpl re-creates locally; a - // separator other than a bare ',' would break the split above. - rendered should include(s"""${ctx.renderInlined(pidField)} as "projects"""") } } diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala index 5ec6b88ab2c..929f403b0a3 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala @@ -21,18 +21,12 @@ package org.apache.texera.web.resource.dashboard.file import org.apache.texera.auth.SessionUser import org.apache.texera.dao.MockTexeraDB -import org.apache.texera.dao.jooq.generated.Tables.{USER, WORKFLOW, WORKFLOW_OF_PROJECT} +import org.apache.texera.dao.jooq.generated.Tables.{USER, WORKFLOW} import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} import org.apache.texera.dao.jooq.generated.tables.daos.{UserDao, WorkflowUserAccessDao} -import org.apache.texera.dao.jooq.generated.tables.pojos.{ - Project, - User, - Workflow, - WorkflowUserAccess -} +import org.apache.texera.dao.jooq.generated.tables.pojos.{User, Workflow, WorkflowUserAccess} import org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.CoverImageRequest -import org.apache.texera.web.resource.dashboard.user.project.ProjectResource import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.{ DashboardWorkflow, @@ -121,13 +115,6 @@ class WorkflowResourceSpec workflow } - private val testProject1: Project = { - val project = new Project() - project.setName("test_project1") - project.setDescription("this is project description") - project - } - private val exampleEmailAddress = "name@example.com" private val exampleWord1 = "Lorem" private val exampleWord2 = "Ipsum" @@ -153,10 +140,6 @@ class WorkflowResourceSpec new WorkflowResource() } - private val projectResource: ProjectResource = { - new ProjectResource() - } - private val dashboardResource: DashboardResource = { new DashboardResource() } @@ -181,7 +164,7 @@ class WorkflowResourceSpec var workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser1) workflows.foreach(workflow => workflowResource.deleteWorkflow( - WorkflowIDs(List(workflow.workflow.getWid), None), + WorkflowIDs(List(workflow.workflow.getWid)), sessionUser1 ) ) @@ -189,18 +172,10 @@ class WorkflowResourceSpec workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser2) workflows.foreach(workflow => workflowResource.deleteWorkflow( - WorkflowIDs(List(workflow.workflow.getWid), None), + WorkflowIDs(List(workflow.workflow.getWid)), sessionUser2 ) ) - - // delete all projects in the database - var projects = projectResource.getProjectList(sessionUser1) - projects.forEach(project => projectResource.deleteProject(project.pid)) - - projects = projectResource.getProjectList(sessionUser2) - projects.forEach(project => projectResource.deleteProject(project.pid)) - } override protected def afterAll(): Unit = { @@ -544,27 +519,6 @@ class WorkflowResourceSpec assert(ownerFilter.toString == USER.EMAIL.eq("owner1").or(USER.EMAIL.eq("owner2")).toString) } - it should "return a proper condition for a single projectId" in { - val projectIdList = new java.util.ArrayList[Integer](util.Arrays.asList(Integer.valueOf(1))) - val projectFilter: Condition = - FulltextSearchQueryUtils.getContainsFilter(projectIdList, WORKFLOW_OF_PROJECT.PID) - assert(projectFilter.toString == WORKFLOW_OF_PROJECT.PID.eq(Integer.valueOf(1)).toString) - } - - it should "return a proper condition for multiple projectIds" in { - val projectIdList = new java.util.ArrayList[Integer]( - util.Arrays.asList(Integer.valueOf(1), Integer.valueOf(2)) - ) - val projectFilter: Condition = - FulltextSearchQueryUtils.getContainsFilter(projectIdList, WORKFLOW_OF_PROJECT.PID) - assert( - projectFilter.toString == WORKFLOW_OF_PROJECT.PID - .eq(Integer.valueOf(1)) - .or(WORKFLOW_OF_PROJECT.PID.eq(Integer.valueOf(2))) - .toString - ) - } - it should "return a proper condition for a single workflowID" in { val workflowIdList = new java.util.ArrayList[Integer](util.Arrays.asList(Integer.valueOf(1))) val workflowIdFilter: Condition = @@ -666,10 +620,8 @@ class WorkflowResourceSpec ) } - "/search API" should "be able to search for resources in different tables" in { + "/search API" should "be able to search for resources by keyword" in { - // create different types of resources, project, workflow, and file - projectResource.createProject(sessionUser1, "test project1") workflowResource.persistWorkflow(testWorkflow1, sessionUser1) // search val DashboardClickableFileEntryList = @@ -677,88 +629,83 @@ class WorkflowResourceSpec sessionUser1, SearchQueryParams(getKeywordsArray("test")) ) - assert(DashboardClickableFileEntryList.results.length == 2) + assert(DashboardClickableFileEntryList.results.length == 1) } it should "return all resources when no keyword provided" in { - projectResource.createProject(sessionUser1, "test project1") workflowResource.persistWorkflow(testWorkflow1, sessionUser1) val DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, SearchQueryParams(getKeywordsArray("")) ) - assert(DashboardClickableFileEntryList.results.length == 2) + assert(DashboardClickableFileEntryList.results.length == 1) } it should "return multiple matching resources from a single resource type" in { workflowResource.persistWorkflow(testWorkflow1, sessionUser1) - projectResource.createProject(sessionUser1, "common project1") - projectResource.createProject(sessionUser1, "common project2") + workflowResource.persistWorkflow(testWorkflow2, sessionUser1) val DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, - SearchQueryParams(getKeywordsArray("common")) + SearchQueryParams(getKeywordsArray("test")) ) assert(DashboardClickableFileEntryList.results.length == 2) } it should "handle multiple keywords correctly" in { - projectResource.createProject(sessionUser1, "test project1") workflowResource.persistWorkflow(testWorkflow1, sessionUser1) + workflowResource.persistWorkflow(testWorkflow2, sessionUser1) val DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, - SearchQueryParams(getKeywordsArray("test", "project1")) + SearchQueryParams(getKeywordsArray("test", "workflow1")) ) assert( DashboardClickableFileEntryList.results.length == 1 - ) // should only return the project + ) // should only return test_workflow1 } it should "filter results by different resourceType" in { - // create different types of resources - // 3 projects, 2 file, and 1 workflow, - projectResource.createProject(sessionUser1, "test project1") - projectResource.createProject(sessionUser1, "test project2") - projectResource.createProject(sessionUser1, "test project3") + // create 3 workflows workflowResource.persistWorkflow(testWorkflow1, sessionUser1) + workflowResource.persistWorkflow(testWorkflow2, sessionUser1) + workflowResource.persistWorkflow(testWorkflow3, sessionUser1) // search resources with all resourceType var DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, SearchQueryParams(getKeywordsArray("test")) ) - assert(DashboardClickableFileEntryList.results.length == 4) + assert(DashboardClickableFileEntryList.results.length == 3) // filter resources by workflow DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, SearchQueryParams(resourceType = "workflow", keywords = getKeywordsArray("test")) ) - assert(DashboardClickableFileEntryList.results.length == 1) + assert(DashboardClickableFileEntryList.results.length == 3) - // filter resources by project + // filter resources by dataset DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, - SearchQueryParams(resourceType = "project", keywords = getKeywordsArray("test")) + SearchQueryParams(resourceType = "dataset", keywords = getKeywordsArray("test")) ) - assert(DashboardClickableFileEntryList.results.length == 3) + assert(DashboardClickableFileEntryList.results.isEmpty) } it should "return resources that match any of all provided keywords" in { // This test is designed to verify that the searchAllResources function correctly // returns resources that match all of the provided keywords - // Create different types of resources, a project, a workflow, and a file - projectResource.createProject(sessionUser1, "test project") workflowResource.persistWorkflow(testWorkflow1, sessionUser1) + workflowResource.persistWorkflow(testWorkflow2, sessionUser1) // Perform search with multiple keywords val DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall( sessionUser1, - SearchQueryParams(keywords = getKeywordsArray("test", "project")) + SearchQueryParams(keywords = getKeywordsArray("test", "workflow2")) ) // Assert that the search results include resources that match any of the provided keywords @@ -768,8 +715,8 @@ class WorkflowResourceSpec it should "not return resources that belong to a different user" in { // This test is designed to verify that the searchAllResources function does not return resources that belong to a different user - // Create a project for a different user (sessionUser2) - projectResource.createProject(sessionUser2, "test project2") + // Create a workflow for a different user (sessionUser2) + workflowResource.persistWorkflow(testWorkflow1, sessionUser2) // Perform search for resources using sessionUser1 val DashboardClickableFileEntryList = @@ -778,7 +725,7 @@ class WorkflowResourceSpec SearchQueryParams(keywords = getKeywordsArray("test")) ) - // Assert that the search results do not include the project that belongs to the different user + // Assert that the search results do not include the workflow that belongs to the different user // Assuming that DashboardClickableFileEntryList is a list of resources where each resource has a `user` property assert(DashboardClickableFileEntryList.results.isEmpty) } @@ -786,10 +733,13 @@ class WorkflowResourceSpec it should "paginate results correctly" in { // This test is designed to verify that the pagination works correctly - // Create 1 workflow, 10 projects - workflowResource.persistWorkflow(testWorkflow1, sessionUser1) - for (i <- 1 to 10) { - projectResource.createProject(sessionUser1, s"test project $i") + // Create 11 workflows + for (i <- 1 to 11) { + val workflow = new Workflow() + workflow.setName(s"test_pagination_workflow$i") + workflow.setDescription("") + workflow.setContent(exampleContent) + workflowResource.persistWorkflow(workflow, sessionUser1) } // Request the first page of results (page size is 10) @@ -1035,7 +985,7 @@ class WorkflowResourceSpec "{\"operators\":[{\"operatorID\":\"op1\",\"operatorType\":\"CSVFileScan\"}]}" ).workflow.getWid - val copies = workflowResource.duplicateWorkflow(WorkflowIDs(List(wid), None), sessionUser1) + val copies = workflowResource.duplicateWorkflow(WorkflowIDs(List(wid)), sessionUser1) assert(copies.size == 1) assert(copies.head.workflow.getName == "dup-src_copy") diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala index 771250ced88..020f4f8ab6a 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala @@ -897,7 +897,6 @@ class HubResourceSpec entry.ownerName shouldBe "hub_owner" entry.ownerId shouldBe Integer.valueOf(ownerUid) entry.accessLevel shouldBe "WRITE" - entry.projectIDs shouldBe empty entry.coverImage shouldBe empty } diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala deleted file mode 100644 index eecb6ff3cbe..00000000000 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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.web.resource.dashboard.user.project - -import org.apache.texera.auth.SessionUser -import org.apache.texera.dao.MockTexeraDB -import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS, USER} -import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} -import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectUserAccessDao, UserDao} -import org.apache.texera.dao.jooq.generated.tables.pojos.{ProjectUserAccess, User} -import org.apache.texera.web.model.common.AccessEntry -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} - -import javax.ws.rs.{BadRequestException, ForbiddenException} -import scala.jdk.CollectionConverters._ - -class ProjectAccessResourceSpec - extends AnyFlatSpec - with BeforeAndAfterAll - with BeforeAndAfterEach - with MockTexeraDB { - - private val ownerUid = 7101 - private val readerUid = 7102 - private val writerUid = 7103 - - private var owner: User = _ - private var reader: User = _ - private var writer: User = _ - private var userDao: UserDao = _ - private var projectUserAccessDao: ProjectUserAccessDao = _ - private var projectResource: ProjectResource = _ - private var projectAccessResource: ProjectAccessResource = _ - - override protected def beforeAll(): Unit = { - initializeDBAndReplaceDSLContext() - } - - override protected def beforeEach(): Unit = { - userDao = new UserDao(getDSLContext.configuration()) - projectUserAccessDao = new ProjectUserAccessDao(getDSLContext.configuration()) - projectResource = new ProjectResource() - projectAccessResource = new ProjectAccessResource() - - owner = createUser(ownerUid, "project_owner", "project-owner@test.com") - reader = createUser(readerUid, "project_reader", "project-reader@test.com") - writer = createUser(writerUid, "project_writer", "project-writer@test.com") - - cleanupTestData() - - userDao.insert(owner) - userDao.insert(reader) - userDao.insert(writer) - } - - override protected def afterEach(): Unit = { - cleanupTestData() - } - - override protected def afterAll(): Unit = { - closeConnectionPool() - } - - private def createUser(uid: Int, name: String, email: String): User = { - val user = new User - user.setUid(uid) - user.setName(name) - user.setEmail(email) - user.setPassword("password") - user.setRole(UserRoleEnum.REGULAR) - user - } - - private def cleanupTestData(): Unit = { - getDSLContext - .deleteFrom(PROJECT_USER_ACCESS) - .where(PROJECT_USER_ACCESS.UID.in(ownerUid, readerUid, writerUid)) - .execute() - - getDSLContext - .deleteFrom(PROJECT) - .where(PROJECT.OWNER_ID.eq(ownerUid)) - .execute() - - getDSLContext - .deleteFrom(USER) - .where(USER.UID.in(ownerUid, readerUid, writerUid)) - .execute() - } - - "ProjectAccessResource.getProjectAccessPrivilege" should "return WRITE if granted" in { - val project = projectResource.createProject(new SessionUser(owner), "write-project") - val privilege = ProjectAccessResource.getProjectAccessPrivilege(project.getPid, ownerUid) - - assert(privilege == PrivilegeEnum.WRITE) - assert(ProjectAccessResource.userHasWriteAccess(project.getPid, ownerUid)) - } - - it should "return READ if a project access row grants READ" in { - val project = projectResource.createProject(new SessionUser(owner), "read-project") - projectUserAccessDao.merge( - new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ) - ) - - val privilege = ProjectAccessResource.getProjectAccessPrivilege(project.getPid, readerUid) - - assert(privilege == PrivilegeEnum.READ) - assert(!ProjectAccessResource.userHasWriteAccess(project.getPid, readerUid)) - } - - it should "return NONE if the user only has access to another project" in { - val sharedProject = projectResource.createProject(new SessionUser(owner), "shared-project") - val privateProject = projectResource.createProject(new SessionUser(owner), "private-project") - projectUserAccessDao.merge( - new ProjectUserAccess(readerUid, sharedProject.getPid, PrivilegeEnum.READ) - ) - - val privilege = - ProjectAccessResource.getProjectAccessPrivilege(privateProject.getPid, readerUid) - - assert(privilege == PrivilegeEnum.NONE) - assert(!ProjectAccessResource.userHasWriteAccess(privateProject.getPid, readerUid)) - } - - it should "return WRITE and grant write access for a WRITE grantee" in { - val project = projectResource.createProject(new SessionUser(owner), "writer-project") - projectUserAccessDao.merge( - new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) - ) - - assert( - ProjectAccessResource.getProjectAccessPrivilege( - project.getPid, - writerUid - ) == PrivilegeEnum.WRITE - ) - assert(ProjectAccessResource.userHasWriteAccess(project.getPid, writerUid)) - } - - "ProjectAccessResource.getOwner" should "return the owning user's email" in { - val project = projectResource.createProject(new SessionUser(owner), "owned-project") - assert(projectAccessResource.getOwner(project.getPid) == owner.getEmail) - } - - "ProjectAccessResource.getAccessList" should "be empty when only the owner has access" in { - val project = projectResource.createProject(new SessionUser(owner), "solo-project") - // createProject grants the owner WRITE, but getAccessList excludes the owner. - assert(projectAccessResource.getAccessList(project.getPid).asScala.isEmpty) - } - - it should "list every grantee (excluding the owner) with their email, name and privilege" in { - val project = projectResource.createProject(new SessionUser(owner), "shared-list-project") - projectUserAccessDao.merge(new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ)) - projectUserAccessDao.merge( - new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) - ) - - val entries = projectAccessResource.getAccessList(project.getPid).asScala.toList - assert(entries.size == 2) - assert(!entries.map(_.email).contains(owner.getEmail)) // owner is excluded - assert(entries.contains(AccessEntry(reader.getEmail, reader.getName, PrivilegeEnum.READ))) - assert(entries.contains(AccessEntry(writer.getEmail, writer.getName, PrivilegeEnum.WRITE))) - } - - "ProjectAccessResource.grantAccess" should "let a WRITE grantee grant READ access to another user" in { - val project = projectResource.createProject(new SessionUser(owner), "grant-project") - // writer is a non-owner WRITE grantee, so it is allowed to grant access. - projectUserAccessDao.merge( - new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) - ) - - projectAccessResource.grantAccess( - project.getPid, - reader.getEmail, - "READ", - new SessionUser(writer) - ) - - assert( - ProjectAccessResource.getProjectAccessPrivilege( - project.getPid, - readerUid - ) == PrivilegeEnum.READ - ) - } - - it should "reject granting to a placeholder account" in { - val project = projectResource.createProject(new SessionUser(owner), "grant-placeholder-project") - val placeholder = new User - placeholder.setName("pj_placeholder") - placeholder.setEmail("pj-placeholder@test.com") - placeholder.setRole(UserRoleEnum.INACTIVE) - placeholder.setIsPlaceholder(true) - userDao.insert(placeholder) - try { - assertThrows[BadRequestException]( - projectAccessResource.grantAccess( - project.getPid, - "pj-placeholder@test.com", - "READ", - new SessionUser(owner) - ) - ) - } finally { - getDSLContext.deleteFrom(USER).where(USER.UID.eq(placeholder.getUid)).execute() - } - } - - it should "reject a user without write access with ForbiddenException" in { - val project = projectResource.createProject(new SessionUser(owner), "grant-forbidden-project") - // reader has no access to the project, so cannot grant. - assertThrows[ForbiddenException]( - projectAccessResource.grantAccess( - project.getPid, - writer.getEmail, - "READ", - new SessionUser(reader) - ) - ) - } - - "ProjectAccessResource.revokeAccess" should "remove a grantee's access" in { - val project = projectResource.createProject(new SessionUser(owner), "revoke-project") - projectUserAccessDao.merge(new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ)) - - projectAccessResource.revokeAccess(project.getPid, reader.getEmail, new SessionUser(owner)) - - assert( - ProjectAccessResource.getProjectAccessPrivilege( - project.getPid, - readerUid - ) == PrivilegeEnum.NONE - ) - } - - it should "reject a user without write access with ForbiddenException" in { - val project = projectResource.createProject(new SessionUser(owner), "revoke-forbidden-project") - projectUserAccessDao.merge( - new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE) - ) - - // reader has no write access, so cannot revoke the writer's access. - assertThrows[ForbiddenException]( - projectAccessResource.revokeAccess(project.getPid, writer.getEmail, new SessionUser(reader)) - ) - } -} diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala deleted file mode 100644 index f97d3d45197..00000000000 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala +++ /dev/null @@ -1,367 +0,0 @@ -/* - * 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.web.resource.dashboard.user.project - -import org.apache.texera.auth.SessionUser -import org.apache.texera.dao.MockTexeraDB -import org.apache.texera.dao.jooq.generated.Tables._ -import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum -import org.apache.texera.dao.jooq.generated.tables.daos.{ - UserDao, - WorkflowDao, - WorkflowOfProjectDao, - WorkflowOfUserDao, - WorkflowUserAccessDao -} -import org.apache.texera.dao.jooq.generated.tables.pojos.{ - User, - Workflow, - WorkflowOfProject, - WorkflowOfUser, - WorkflowUserAccess -} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} - -import java.sql.Timestamp -import java.util.UUID -import javax.ws.rs.{BadRequestException, ForbiddenException} -import scala.jdk.CollectionConverters._ - -class ProjectResourceSpec - extends AnyFlatSpec - with Matchers - with BeforeAndAfterAll - with BeforeAndAfterEach - with MockTexeraDB { - - private val ownerUid = 12000 + scala.util.Random.nextInt(1000) - private val strangerUid = 14000 + scala.util.Random.nextInt(1000) - private val testUids = Seq(ownerUid, strangerUid) - - private var owner: User = _ - private var stranger: User = _ - private var workflowOfProjectDao: WorkflowOfProjectDao = _ - private var userDao: UserDao = _ - private var workflowDao: WorkflowDao = _ - private var workflowOfUserDao: WorkflowOfUserDao = _ - private var workflowUserAccessDao: WorkflowUserAccessDao = _ - private var resource: ProjectResource = _ - - override protected def beforeAll(): Unit = { - initializeDBAndReplaceDSLContext() - } - - override protected def beforeEach(): Unit = { - workflowOfProjectDao = new WorkflowOfProjectDao(getDSLContext.configuration()) - userDao = new UserDao(getDSLContext.configuration()) - workflowDao = new WorkflowDao(getDSLContext.configuration()) - workflowOfUserDao = new WorkflowOfUserDao(getDSLContext.configuration()) - workflowUserAccessDao = new WorkflowUserAccessDao(getDSLContext.configuration()) - resource = new ProjectResource() - - owner = makeUser(ownerUid, "proj_owner") - stranger = makeUser(strangerUid, "proj_stranger") - - cleanupTestData() - userDao.insert(owner) - userDao.insert(stranger) - } - - override protected def afterEach(): Unit = { - cleanupTestData() - } - - override protected def afterAll(): Unit = { - closeConnectionPool() - } - - private def cleanupTestData(): Unit = { - val ctx = getDSLContext - // projects owned by our test users (pids are DB-generated) - val pids = ctx - .select(PROJECT.PID) - .from(PROJECT) - .where(PROJECT.OWNER_ID.in(testUids.map(Integer.valueOf): _*)) - .fetchInto(classOf[Integer]) - .asScala - .toList - // workflows our test users came to own - val wids = ctx - .select(WORKFLOW_OF_USER.WID) - .from(WORKFLOW_OF_USER) - .where(WORKFLOW_OF_USER.UID.in(testUids.map(Integer.valueOf): _*)) - .fetchInto(classOf[Integer]) - .asScala - .toList - - if (pids.nonEmpty) { - ctx.deleteFrom(WORKFLOW_OF_PROJECT).where(WORKFLOW_OF_PROJECT.PID.in(pids: _*)).execute() - ctx.deleteFrom(PROJECT_USER_ACCESS).where(PROJECT_USER_ACCESS.PID.in(pids: _*)).execute() - } - ctx - .deleteFrom(PROJECT_USER_ACCESS) - .where(PROJECT_USER_ACCESS.UID.in(testUids.map(Integer.valueOf): _*)) - .execute() - ctx.deleteFrom(PROJECT).where(PROJECT.OWNER_ID.in(testUids.map(Integer.valueOf): _*)).execute() - - if (wids.nonEmpty) { - ctx.deleteFrom(WORKFLOW_OF_PROJECT).where(WORKFLOW_OF_PROJECT.WID.in(wids: _*)).execute() - ctx.deleteFrom(WORKFLOW_USER_ACCESS).where(WORKFLOW_USER_ACCESS.WID.in(wids: _*)).execute() - ctx.deleteFrom(WORKFLOW_OF_USER).where(WORKFLOW_OF_USER.WID.in(wids: _*)).execute() - ctx.deleteFrom(WORKFLOW).where(WORKFLOW.WID.in(wids: _*)).execute() - } - ctx.deleteFrom(USER).where(USER.UID.in(testUids.map(Integer.valueOf): _*)).execute() - } - - private def makeUser(uid: Int, name: String): User = { - val user = new User - user.setUid(Integer.valueOf(uid)) - user.setName(name) - user.setEmail(s"$name@test.com") - user.setPassword("password") - user - } - - private def session(user: User): SessionUser = new SessionUser(user) - - /** Seeds a workflow owned by the given user with WRITE access (so hasReadAccess passes). */ - private def seedWorkflow(uid: Int): Integer = { - val wid = Integer.valueOf(16000 + scala.util.Random.nextInt(100000)) - val workflow = new Workflow - workflow.setWid(wid) - workflow.setName("wf_" + UUID.randomUUID().toString.substring(0, 8)) - workflow.setContent("""{"operators":[],"links":[]}""") - workflow.setDescription("") - workflow.setIsPublic(false) - workflow.setCreationTime(new Timestamp(System.currentTimeMillis())) - workflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis())) - workflowDao.insert(workflow) - - val ownership = new WorkflowOfUser - ownership.setUid(Integer.valueOf(uid)) - ownership.setWid(wid) - workflowOfUserDao.insert(ownership) - - val access = new WorkflowUserAccess - access.setUid(Integer.valueOf(uid)) - access.setWid(wid) - access.setPrivilege(PrivilegeEnum.WRITE) - workflowUserAccessDao.insert(access) - wid - } - - private def workflowOfProjectCount(wid: Integer, pid: Integer): Int = - getDSLContext.fetchCount( - WORKFLOW_OF_PROJECT, - WORKFLOW_OF_PROJECT.WID.eq(wid).and(WORKFLOW_OF_PROJECT.PID.eq(pid)) - ) - - behavior of "ProjectResource" - - it should "create a project owned by the user and make it retrievable with a WRITE access row" in { - val created = resource.createProject(session(owner), "my_project") - - created.getName shouldBe "my_project" - created.getOwnerId shouldBe Integer.valueOf(ownerUid) - resource.getProject(created.getPid).getName shouldBe "my_project" - - val privilege = getDSLContext - .select(PROJECT_USER_ACCESS.PRIVILEGE) - .from(PROJECT_USER_ACCESS) - .where( - PROJECT_USER_ACCESS.PID - .eq(created.getPid) - .and(PROJECT_USER_ACCESS.UID.eq(Integer.valueOf(ownerUid))) - ) - .fetchOne(0, classOf[PrivilegeEnum]) - privilege shouldBe PrivilegeEnum.WRITE - } - - it should "list no projects for a user who owns none and all projects once created" in { - resource.getProjectList(session(stranger)).asScala shouldBe empty - - resource.createProject(session(owner), "p1") - resource.createProject(session(owner), "p2") - - resource.getProjectList(session(owner)).asScala.map(_.name).toSet shouldBe Set("p1", "p2") - } - - it should "rename a project and reject a blank name" in { - val pid = resource.createProject(session(owner), "before").getPid - - resource.updateProjectName(pid, "after") - resource.getProject(pid).getName shouldBe "after" - - assertThrows[BadRequestException] { - resource.updateProjectName(pid, " ") - } - // the rejected rename left the previous value intact - resource.getProject(pid).getName shouldBe "after" - } - - it should "update a project description via a re-read" in { - val pid = resource.createProject(session(owner), "p").getPid - - resource.updateProjectDescription(pid, "a new description") - - resource.getProject(pid).getDescription shouldBe "a new description" - } - - it should "add a workflow to a project, stay idempotent, and reject a user without access" in { - val pid = resource.createProject(session(owner), "p").getPid - val wid = seedWorkflow(ownerUid) - - resource.addWorkflowToProject(pid, wid, session(owner)) - workflowOfProjectCount(wid, pid) shouldBe 1 - - // a second add for the same pair must not create a duplicate mapping - resource.addWorkflowToProject(pid, wid, session(owner)) - workflowOfProjectCount(wid, pid) shouldBe 1 - - // the stranger has no access to this workflow - assertThrows[ForbiddenException] { - resource.addWorkflowToProject(pid, wid, session(stranger)) - } - } - - it should "remove a workflow-to-project mapping" in { - val pid = resource.createProject(session(owner), "p").getPid - val wid = seedWorkflow(ownerUid) - resource.addWorkflowToProject(pid, wid, session(owner)) - workflowOfProjectCount(wid, pid) shouldBe 1 - - resource.deleteWorkflowFromProject(pid, wid) - - workflowOfProjectCount(wid, pid) shouldBe 0 - } - - it should "accept both 3- and 6-digit hex colours and persist the last one" in { - val pid = resource.createProject(session(owner), "p").getPid - - resource.updateProjectColor(pid, "AABBCC", session(owner)) - resource.getProject(pid).getColor shouldBe "AABBCC" - - // The shorthand form is legal too, and the value is stored verbatim rather than expanded. - resource.updateProjectColor(pid, "f0a", session(owner)) - resource.getProject(pid).getColor shouldBe "f0a" - } - - it should "reject colours that are not 3 or 6 hex digits, leaving the stored one intact" in { - val pid = resource.createProject(session(owner), "p").getPid - resource.updateProjectColor(pid, "123456", session(owner)) - - // Wrong length, and a right-length value with a non-hex digit: this exercises both the - // length and hex-digit validation branches in updateProjectColor. - Seq("12345", "1234567", "GGGGGG", "12G", "").foreach { bad => - withClue(s"colour '$bad': ") { - assertThrows[BadRequestException] { - resource.updateProjectColor(pid, bad, session(owner)) - } - } - } - - resource.getProject(pid).getColor shouldBe "123456" - } - - it should "reject a null colour before dereferencing it" in { - val pid = resource.createProject(session(owner), "p").getPid - - // The null check has to come first; without it the length read is an NPE rather than a 400. - assertThrows[BadRequestException] { - resource.updateProjectColor(pid, null, session(owner)) - } - } - - it should "clear a project's colour" in { - val pid = resource.createProject(session(owner), "p").getPid - resource.updateProjectColor(pid, "ABCDEF", session(owner)) - - resource.deleteProjectColor(pid) - - resource.getProject(pid).getColor shouldBe null - } - - it should "list only the workflows belonging to the given project" in { - val pid = resource.createProject(session(owner), "p").getPid - val other = resource.createProject(session(owner), "other").getPid - val inProject = seedWorkflow(ownerUid) - val elsewhere = seedWorkflow(ownerUid) - resource.addWorkflowToProject(pid, inProject, session(owner)) - resource.addWorkflowToProject(other, elsewhere, session(owner)) - - // Two projects each holding one workflow, so a filter that ignored the pid would return both. - resource.listProjectWorkflows(pid, session(owner)).map(_.workflow.getWid) shouldBe List( - inProject - ) - resource.listProjectWorkflows(other, session(owner)).map(_.workflow.getWid) shouldBe List( - elsewhere - ) - } - - it should "return no workflows for a project that holds none" in { - val pid = resource.createProject(session(owner), "empty").getPid - - resource.listProjectWorkflows(pid, session(owner)) shouldBe empty - } - - it should "delete a project" in { - val pid = resource.createProject(session(owner), "doomed").getPid - resource.getProject(pid) should not be null - - resource.deleteProject(pid) - - resource.getProject(pid) shouldBe null - } - - behavior of "ProjectResource.addExportedFileToProject" - - it should "return an empty status when the workflow belongs to no project" in { - val wid = seedWorkflow(ownerUid) - ProjectResource.addExportedFileToProject(Integer.valueOf(ownerUid), wid, "out.csv") shouldBe "" - } - - it should "name the single project the workflow belongs to" in { - val wid = seedWorkflow(ownerUid) - val pid = resource.createProject(session(owner), "only_project").getPid - workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid)) - - ProjectResource.addExportedFileToProject( - Integer.valueOf(ownerUid), - wid, - "out.csv" - ) shouldBe "and added to project: only_project" - } - - it should "list every project the workflow belongs to when there are several" in { - val wid = seedWorkflow(ownerUid) - val pid1 = resource.createProject(session(owner), "alpha").getPid - val pid2 = resource.createProject(session(owner), "beta").getPid - workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid1)) - workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid2)) - - val status = ProjectResource.addExportedFileToProject(Integer.valueOf(ownerUid), wid, "out.csv") - - status should startWith("and added to projects: ") - status should include("alpha") - status should include("beta") - } -} diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala deleted file mode 100644 index 4d532a9d44e..00000000000 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.web.resource.dashboard.user.project - -import org.apache.texera.auth.SessionUser -import org.apache.texera.dao.MockTexeraDB -import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS, PUBLIC_PROJECT} -import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} -import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectDao, UserDao} -import org.apache.texera.dao.jooq.generated.tables.pojos.{Project, User} -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import java.util -import java.util.UUID -import scala.jdk.CollectionConverters._ - -class PublicProjectResourceSpec - extends AnyFlatSpec - with Matchers - with BeforeAndAfterAll - with BeforeAndAfterEach - with MockTexeraDB { - - // MockTexeraDB gives each suite its own database, so a fixed uid is isolated and reproducible. - private val testUid = 90001 - private var projectDao: ProjectDao = _ - private var sessionUser: SessionUser = _ - private val resource = new PublicProjectResource - - private def makeUser(uid: Int, name: String): User = { - val user = new User - user.setUid(uid) - user.setName(name) - user.setEmail( - s"public_project_spec_${uid}_${UUID.randomUUID().toString.substring(0, 8)}@example.com" - ) - user.setPassword("password") - user.setRole(UserRoleEnum.ADMIN) - user - } - - override protected def beforeAll(): Unit = { - initializeDBAndReplaceDSLContext() - val user = makeUser(testUid, "public_project_owner") - new UserDao(getDSLContext.configuration()).insert(user) - sessionUser = new SessionUser(user) - projectDao = new ProjectDao(getDSLContext.configuration()) - } - - override protected def afterAll(): Unit = closeConnectionPool() - - // Remove the per-test project rows (children before parents) after each test; the owner - // user seeded once in beforeAll is intentionally kept for the whole suite. - override protected def afterEach(): Unit = { - getDSLContext.deleteFrom(PUBLIC_PROJECT).where(PUBLIC_PROJECT.UID.eq(testUid)).execute() - getDSLContext - .deleteFrom(PROJECT_USER_ACCESS) - .where(PROJECT_USER_ACCESS.UID.eq(testUid)) - .execute() - getDSLContext.deleteFrom(PROJECT).where(PROJECT.OWNER_ID.eq(testUid)).execute() - } - - // Insert a project owned by the test user; the generated pid is populated on the pojo. - private def seedProject(name: String): Project = { - val project = new Project(null, name, null, Integer.valueOf(testUid), null, null) - projectDao.insert(project) - project - } - - "getType" should "report a project without a public flag as Private" in { - val project = seedProject("p_private") - resource.getType(project.getPid) shouldBe "Private" - } - - "makePublic" should "flag a project public so getType reports Public" in { - val project = seedProject("p_makepublic") - resource.makePublic(project.getPid, sessionUser) - resource.getType(project.getPid) shouldBe "Public" - } - - "makePrivate" should "revert a public project so getType reports Private again" in { - val project = seedProject("p_makeprivate") - resource.makePublic(project.getPid, sessionUser) - resource.getType(project.getPid) shouldBe "Public" - - resource.makePrivate(project.getPid) - resource.getType(project.getPid) shouldBe "Private" - } - - "listPublicProjects" should "return an empty list when no project is public" in { - seedProject("p_still_private") - resource.listPublicProjects().asScala shouldBe empty - } - - it should "return only the currently-public projects with their name and owner" in { - val publicProject = seedProject("p_public") - seedProject("p_hidden") - resource.makePublic(publicProject.getPid, sessionUser) - - val listed = resource.listPublicProjects().asScala - listed.map(_.pid) shouldBe Seq(publicProject.getPid) - listed.head.name shouldBe "p_public" - listed.head.owner shouldBe "public_project_owner" - } - - // NOTE: despite the name, addPublicProjects does NOT set the public flag — it grants the - // caller READ access to each project (a ProjectUserAccess row) and leaves getType Private. - "addPublicProjects" should "grant the caller READ access to each listed project" in { - val p1 = seedProject("p_add1") - val p2 = seedProject("p_add2") - - resource.addPublicProjects(util.Arrays.asList(p1.getPid, p2.getPid), sessionUser) - - val grantedPids = getDSLContext - .select(PROJECT_USER_ACCESS.PID) - .from(PROJECT_USER_ACCESS) - .where( - PROJECT_USER_ACCESS.UID - .eq(testUid) - .and(PROJECT_USER_ACCESS.PRIVILEGE.eq(PrivilegeEnum.READ)) - ) - .fetchInto(classOf[Integer]) - .asScala - grantedPids should contain allOf (p1.getPid, p2.getPid) - resource.getType(p1.getPid) shouldBe "Private" // access grant is not the public flag - } -} diff --git a/common/config/src/main/resources/default.conf b/common/config/src/main/resources/default.conf index 6b83f50f6b3..1a17e89aed0 100644 --- a/common/config/src/main/resources/default.conf +++ b/common/config/src/main/resources/default.conf @@ -55,9 +55,6 @@ gui { your_work_enabled = true your_work_enabled = ${?GUI_TABS_YOUR_WORK_ENABLED} - projects_enabled = false - projects_enabled = ${?GUI_TABS_PROJECTS_ENABLED} - workflows_enabled = true workflows_enabled = ${?GUI_TABS_WORKFLOWS_ENABLED} diff --git a/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala b/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala index 86449e0a11d..5948a1f28d5 100644 --- a/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala +++ b/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala @@ -84,7 +84,6 @@ object MockTexeraDB { val replacementText = """CREATE INDEX idx_workflow_name_description_content ON workflow USING GIN (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '') || ' ' || COALESCE(content, ''))); |CREATE INDEX idx_user_name ON "user" USING GIN (to_tsvector('english', COALESCE(name, ''))); - |CREATE INDEX idx_user_project_name_description ON project USING GIN (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, ''))); |CREATE INDEX idx_dataset_name_description ON dataset USING GIN (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, ''))); |CREATE INDEX idx_dataset_version_name ON dataset_version USING GIN (to_tsvector('english', COALESCE(name, '')));""".stripMargin diff --git a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala index f27f9463d8e..eddaa13aa1a 100644 --- a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala +++ b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala @@ -416,7 +416,6 @@ class ConfigResourceSpec "workflow_enabled", "dataset_enabled", "your_work_enabled", - "projects_enabled", "workflows_enabled", "datasets_enabled", "compute_enabled", diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala index bef9c38cc21..e439ad35a4f 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala @@ -29,7 +29,7 @@ object WorkflowAccessResource { /** * Whether the given user holds a direct WRITE grant on the given workflow. * - * Only the direct WORKFLOW_USER_ACCESS grant is consulted — no project or + * Only the direct WORKFLOW_USER_ACCESS grant is consulted — no * public-visibility fallback — so the notebook endpoints stay self-contained. * * @param wid workflow id diff --git a/sql/changelog.xml b/sql/changelog.xml index bdba73339de..9108e1804cf 100644 --- a/sql/changelog.xml +++ b/sql/changelog.xml @@ -68,6 +68,11 @@ + + + + +