Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.apache.texera.web.resource.dashboard

import org.apache.texera.auth.util.OwnerAccess
import org.apache.texera.dao.jooq.generated.Tables._
import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
import org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry
Expand Down Expand Up @@ -100,7 +101,7 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
if (uid == null) {
condition = WORKFLOW.IS_PUBLIC.eq(true)
} else {
val privateAccessCondition = WORKFLOW_USER_ACCESS.UID.eq(uid)
val privateAccessCondition = grantedAccessCondition(uid)
if (includePublic) {
condition = privateAccessCondition.or(WORKFLOW.IS_PUBLIC.eq(true))
} else {
Expand All @@ -111,10 +112,10 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
baseQuery.where(condition)
}

/** Rows the user was granted access to, as opposed to rows they see only because it is public. */
/** Rows the user owns or was granted, as opposed to rows they see only because it is public. */
private def grantedAccessCondition(uid: Integer): Condition =
if (uid == null) DSL.falseCondition()
else WORKFLOW_USER_ACCESS.UID.eq(uid)
else WORKFLOW_USER_ACCESS.UID.eq(uid).or(WORKFLOW_OF_USER.UID.eq(uid))

/**
* The three searchable columns carrying one copy. They travel together because a pin freezes them
Expand Down Expand Up @@ -232,8 +233,10 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
// same copy the detail page serves -- otherwise a listing would advertise a title that opening it
// does not show. Both are NULL while following, which leaves the live values in place.
// Unknown counts as not granted: the reverse is the leak.
val granted = Option(record.get("viewer_has_granted_access", classOf[java.lang.Boolean]))
.exists(_.booleanValue())
val isOwner = uid != null && record.into(WORKFLOW_OF_USER).getUid == uid
val granted = isOwner || Option(
record.get("viewer_has_granted_access", classOf[java.lang.Boolean])
).exists(_.booleanValue())
if (!granted) {
Option(record.get("workflow_published_name", classOf[String])).foreach(workflow.setName)
Option(record.get("workflow_published_description", classOf[String]))
Expand All @@ -243,10 +246,10 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
}

val dw = DashboardWorkflow(
record.into(WORKFLOW_OF_USER).getUid == uid,
Option(record.get(WORKFLOW_USER_ACCESS.PRIVILEGE, classOf[PrivilegeEnum]))
.map(_.toString)
.getOrElse(PrivilegeEnum.NONE.toString),
isOwner,
OwnerAccess
.privilege(isOwner, record.get(WORKFLOW_USER_ACCESS.PRIVILEGE, classOf[PrivilegeEnum]))
.toString,
record.into(USER).getName,
workflow,
record.into(USER).getUid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.apache.texera.auth.SessionUser
import org.apache.texera.dao.SqlServer
import org.apache.texera.dao.jooq.generated.Tables._
import org.apache.texera.web.resource.dashboard.admin.execution.AdminExecutionResource._
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource
import org.jooq.impl.DSL

import javax.annotation.security.RolesAllowed
Expand Down Expand Up @@ -184,10 +185,10 @@ class AdminExecutionResource {

// Retrieve the id of each workflow that the user has access to.
val availableWorkflowIds = context
.select(WORKFLOW_USER_ACCESS.WID)
.from(WORKFLOW_USER_ACCESS)
.where(WORKFLOW_USER_ACCESS.UID.eq(current_user.getUid))
.fetchInto(classOf[Integer])
.fetch(WorkflowAccessResource.accessibleWids(current_user.getUid))
.getValues(0, classOf[Integer])
.asScala
.toSet

// Calculate the statistics needed for each execution.
executions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ object HubResource {
return List.empty[DashboardWorkflow]
}

val records = baseWorkflowSelect()
val records = baseWorkflowSelect(uid)
.where(WORKFLOW.WID.in(wids: _*))
.groupBy(
WORKFLOW.WID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import org.apache.texera.dao.SqlServer
import org.apache.texera.dao.jooq.generated.Tables._
import org.apache.texera.web.resource.dashboard.user.dataset.utils.DatasetStatisticsUtils.getUserCreatedDatasets
import org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource._
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResource
import org.apache.texera.web.resource.dashboard.user.workflow.{
WorkflowAccessResource,
WorkflowExecutionsResource
}

import java.util
import javax.ws.rs._
Expand Down Expand Up @@ -100,19 +103,7 @@ object UserQuotaResource {
}

def getUserAccessedWorkflow(uid: Integer): util.List[Integer] = {
val availableWorkflowIds = context
.select(
WORKFLOW_USER_ACCESS.WID
)
.from(
WORKFLOW_USER_ACCESS
)
.where(
WORKFLOW_USER_ACCESS.UID.eq(uid)
)
.fetchInto(classOf[Integer])

availableWorkflowIds
context.fetch(WorkflowAccessResource.accessibleWids(uid)).getValues(0, classOf[Integer])
}

def getUserQuotaSize(uid: Integer): Array[QuotaStorage] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import org.apache.texera.web.model.common.AccessEntry
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.{
context,
getPrivilege,
hasWriteAccess
hasWriteAccess,
isOwner
}
import org.jooq.DSLContext
import org.jooq.impl.DSL
import org.jooq.{DSLContext, Record1, Select}

import java.util
import javax.annotation.security.RolesAllowed
Expand All @@ -58,10 +60,7 @@ object WorkflowAccessResource {
* @return boolean value indicating yes/no
*/
def hasReadAccess(wid: Integer, uid: Integer): Boolean = {
isPublic(wid) || getPrivilege(wid, uid).eq(PrivilegeEnum.READ) || hasWriteAccess(
wid,
uid
)
isPublic(wid) || !getPrivilege(wid, uid).eq(PrivilegeEnum.NONE)
}

/**
Expand All @@ -81,18 +80,40 @@ object WorkflowAccessResource {
* @return PrivilegeEnum value indicating NONE/READ/WRITE
*/
def getPrivilege(wid: Integer, uid: Integer): PrivilegeEnum = {
val access = context
.select()
.from(WORKFLOW_USER_ACCESS)
.where(WORKFLOW_USER_ACCESS.WID.eq(wid).and(WORKFLOW_USER_ACCESS.UID.eq(uid)))
.fetchOneInto(classOf[WorkflowUserAccess])
if (access == null) {
PrivilegeEnum.NONE
} else {
access.getPrivilege
}
// One round trip: the caller's own ownership row and grant, read together. Both joins are
// narrowed to the caller, so a workflow with several owners still yields one row.
val row = context
.select(WORKFLOW_OF_USER.UID, WORKFLOW_USER_ACCESS.PRIVILEGE)
.from(WORKFLOW)
.leftJoin(WORKFLOW_OF_USER)
.on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID).and(WORKFLOW_OF_USER.UID.eq(uid)))
.leftJoin(WORKFLOW_USER_ACCESS)
.on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW.WID).and(WORKFLOW_USER_ACCESS.UID.eq(uid)))
.where(WORKFLOW.WID.eq(wid))
.fetchOne()
if (row == null) PrivilegeEnum.NONE
else OwnerAccess.privilege(row.value1() != null, row.value2())
}

def isOwner(wid: Integer, uid: Integer): Boolean =
context.fetchExists(
WORKFLOW_OF_USER,
WORKFLOW_OF_USER.WID.eq(wid).and(WORKFLOW_OF_USER.UID.eq(uid))
)
Comment thread
tanishqgandhi1908 marked this conversation as resolved.

/** Ids of the workflows `uid` owns or was granted. */
def accessibleWids(uid: Integer): Select[Record1[Integer]] =
DSL
.select(WORKFLOW_OF_USER.WID)
.from(WORKFLOW_OF_USER)
.where(WORKFLOW_OF_USER.UID.eq(uid))
.union(
DSL
.select(WORKFLOW_USER_ACCESS.WID)
.from(WORKFLOW_USER_ACCESS)
.where(WORKFLOW_USER_ACCESS.UID.eq(uid))
)

/**
* Whether the user was granted access to the workflow itself, rather than merely being able to
* read it because it is public. Granted access sees the author's working copy; public access is
Expand Down Expand Up @@ -194,12 +215,7 @@ class WorkflowAccessResource() {
throw new BadRequestException(s"No registered user with email $email")
}
val userUid = targetUser.getUid
val workflowOwnerUid = context
.select(WORKFLOW_OF_USER.UID)
.from(WORKFLOW_OF_USER)
.where(WORKFLOW_OF_USER.WID.eq(wid))
.fetchOneInto(classOf[Integer])
if (userUid == workflowOwnerUid) {
if (isOwner(wid, userUid)) {
// amber is on javax JAX-RS, so it raises its own ForbiddenException with the shared message.
throw new ForbiddenException(OwnerAccess.ownerChangeRefused)
}
Expand Down Expand Up @@ -234,10 +250,9 @@ class WorkflowAccessResource() {
): Unit = {
try {
val targetUserUid = userDao.fetchOneByEmail(email).getUid
val workflowOwnerUid = workflowOfUserDao.fetchByWid(wid).get(0).getUid

// Prevent owner from revoking their own access
if (targetUserUid == workflowOwnerUid) {
if (isOwner(wid, targetUserUid)) {
throw new ForbiddenException("The owner cannot revoke their own access")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.dropwizard.auth.Auth
import org.apache.texera.amber.core.storage.DocumentFactory
import org.apache.texera.amber.core.virtualidentity.ExecutionIdentity
import org.apache.texera.auth.SessionUser
import org.apache.texera.auth.util.OwnerAccess
import org.apache.texera.dao.SqlServer
import org.apache.texera.dao.jooq.generated.Tables._
import org.apache.texera.dao.jooq.generated.enums.{DefaultViewEnum, PrivilegeEnum}
Expand All @@ -41,6 +42,7 @@ import org.apache.texera.web.resource.dashboard.hub.EntityType
import org.apache.texera.web.service.WarehouseReadGuard
import org.apache.texera.web.resource.dashboard.hub.HubResource.recordCloneAction
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource._
import org.jooq.impl.DSL
import org.jooq.impl.DSL.max
import org.jooq.{DSLContext, Record10, Result, SelectOnConditionStep, TableField}

Expand Down Expand Up @@ -108,14 +110,6 @@ object WorkflowResource {
)
}

private def workflowOfUserExists(wid: Integer, uid: Integer): Boolean = {
workflowOfUserDao.existsById(
context
.newRecord(WORKFLOW_OF_USER.UID, WORKFLOW_OF_USER.WID)
.values(uid, wid)
)
}

case class DashboardWorkflow(
isOwner: Boolean,
accessLevel: String,
Expand Down Expand Up @@ -226,12 +220,7 @@ object WorkflowResource {
): Unit = {
val user = sessionUser.getUser

if (
workflowOfUserExists(wid, user.getUid) || WorkflowAccessResource.hasWriteAccess(
wid,
user.getUid
)
) {
if (WorkflowAccessResource.hasWriteAccess(wid, user.getUid)) {
context.update(WORKFLOW).set(field, value).where(WORKFLOW.WID.eq(wid)).execute()
} else {
throw new ForbiddenException("No sufficient access privilege.")
Expand Down Expand Up @@ -266,7 +255,8 @@ object WorkflowResource {
}
}

def baseWorkflowSelect(): SelectOnConditionStep[Record10[
/** The access join is narrowed to `uid`, so each row carries only the viewer's grant. */
def baseWorkflowSelect(uid: Integer): SelectOnConditionStep[Record10[
Integer,
String,
String,
Expand Down Expand Up @@ -294,6 +284,7 @@ object WorkflowResource {
.from(WORKFLOW)
.leftJoin(WORKFLOW_USER_ACCESS)
.on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW.WID))
.and(if (uid == null) DSL.falseCondition() else WORKFLOW_USER_ACCESS.UID.eq(uid))
.leftJoin(WORKFLOW_OF_USER)
.on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID))
.leftJoin(USER)
Expand All @@ -320,12 +311,11 @@ object WorkflowResource {
workflowEntries
.map { workflowRecord =>
val ownerUid = workflowRecord.into(WORKFLOW_OF_USER).getUid
val isOwner = uid != null && ownerUid == uid
DashboardWorkflow(
uid != null && ownerUid == uid,
workflowRecord
.into(WORKFLOW_USER_ACCESS)
.into(classOf[WorkflowUserAccess])
.getPrivilege
isOwner,
OwnerAccess
.privilege(isOwner, workflowRecord.get(WORKFLOW_USER_ACCESS.PRIVILEGE))
.toString,
workflowRecord.into(USER).getName,
workflowRecord.into(WORKFLOW).into(classOf[Workflow]),
Expand All @@ -352,10 +342,8 @@ class WorkflowResource extends LazyLogging {
@Path("/user-workflow-ids")
def retrieveIDs(@Auth user: SessionUser): util.List[String] = {
context
.select(WORKFLOW_USER_ACCESS.WID)
.from(WORKFLOW_USER_ACCESS)
.where(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))
.fetchInto(classOf[String])
.fetch(WorkflowAccessResource.accessibleWids(user.getUid))
.getValues(0, classOf[String])
}

/**
Expand All @@ -369,12 +357,10 @@ class WorkflowResource extends LazyLogging {
def retrieveOwners(@Auth user: SessionUser): util.List[String] = {
context
.selectDistinct(USER.EMAIL)
.from(WORKFLOW_USER_ACCESS)
.join(WORKFLOW_OF_USER)
.on(WORKFLOW_USER_ACCESS.WID.eq(WORKFLOW_OF_USER.WID))
.from(WORKFLOW_OF_USER)
.join(USER)
.on(WORKFLOW_OF_USER.UID.eq(USER.UID))
.where(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))
.where(WORKFLOW_OF_USER.WID.in(WorkflowAccessResource.accessibleWids(user.getUid)))
.fetchInto(classOf[String])
}

Expand All @@ -390,8 +376,8 @@ class WorkflowResource extends LazyLogging {
@Auth sessionUser: SessionUser
): List[DashboardWorkflow] = {
val user = sessionUser.getUser
val workflowEntries = baseWorkflowSelect()
.where(WORKFLOW_USER_ACCESS.UID.eq(user.getUid))
val workflowEntries = baseWorkflowSelect(user.getUid)
.where(WORKFLOW_USER_ACCESS.UID.isNotNull.or(WORKFLOW_OF_USER.UID.eq(user.getUid)))
.groupBy(
WORKFLOW.WID,
WORKFLOW.NAME,
Expand Down Expand Up @@ -460,7 +446,7 @@ class WorkflowResource extends LazyLogging {
def persistWorkflow(workflow: Workflow, @Auth sessionUser: SessionUser): Workflow = {
val user = sessionUser.getUser

if (workflowOfUserExists(workflow.getWid, user.getUid)) {
if (WorkflowAccessResource.isOwner(workflow.getWid, user.getUid)) {
WorkflowVersionResource.insertVersion(workflow, insertingNewWorkflow = false)
saveWorkflowFields(workflow)
} else {
Expand Down Expand Up @@ -669,7 +655,7 @@ class WorkflowResource extends LazyLogging {
// Delete workflows in a transaction
context.transaction { _ =>
for (wid <- workflowIDs.wids) {
if (workflowOfUserExists(wid, user.getUid)) {
if (WorkflowAccessResource.isOwner(wid, user.getUid)) {
workflowDao.deleteById(wid)
} else {
throw new BadRequestException("The workflow does not exist.")
Expand Down
Loading
Loading