Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID-897 filter resources endpoint #1236

Merged
merged 9 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1558,6 +1558,64 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorReport'
/api/resources/v2/filter:
get:
tags:
- Resources
summary: Filter resources the calling user has access to.
This endpoint MUST NOT be used to determine if a user has an action on a resource. This endpoint is ONLY
for determining the state of the user's permissions. It does not take into account enabled status, or Terms of Service Compliance.
To check if a user is allowed to execute an action on a resource, use /api/resources/v2/{resourceTypeName}/{resourceId}/action/{action}
By default, public resources are not included. Including public resources incurs a 3x cost of DB performance.
operationId: filterResourcesV3
parameters:
- name: resourceTypes
in: query
description: Types of resources to filter on
required: false
schema:
type: array
items:
type: string
- name: policies
in: query
description: Names of policies to filter on
required: false
schema:
type: array
items:
type: string
- name: roles
in: query
description: Names of roles to filter on
required: false
schema:
type: array
items:
type: string
- name: actions
in: query
description: Names of actions to filter on
required: false
schema:
type: array
items:
type: string
- name: includePublic
in: query
description: Include public resources in the response
required: false
schema:
type: boolean
responses:
200:
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FilteredResourcesResponse'
/api/resources/v2/{resourceTypeName}:
get:
tags:
Expand Down Expand Up @@ -4048,6 +4106,34 @@ components:
type: boolean
description: Whether or not the user is enabled
description: specification of a UpdateUserRequest
FilteredResourcesResponse:
type: object
properties:
resources:
type: array
items:
$ref: '#/components/schemas/FilteredResource'
FilteredResource:
type: object
properties:
resourceType:
type: string
description: resourceType of the resource
resourceId:
type: string
description: resourceId of the resource
policies:
type: string
description: policies the user is a part of on the resource
roles:
type: string
description: roles the user has on the resource
actions:
type: string
description: actions the user has on the resource
isPublic:
type: string
description: isPublic is the resource public or not
securitySchemes:
googleoauth:
type: oauth2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import cats.effect.IO
import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._
import org.broadinstitute.dsde.workbench.model._
Expand Down Expand Up @@ -114,7 +115,15 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM
}
}
}
} ~ pathPrefix("resources" / "v2") {
} ~
pathPrefix("resources" / "v2") {
pathPrefix("filter") {
pathEndOrSingleSlash {
get {
filterUserResources(samUser, samRequestContext)
}
}
} ~
pathPrefix(Segment) { resourceTypeName =>
withNonAdminResourceType(ResourceTypeName(resourceTypeName)) { resourceType =>
pathEndOrSingleSlash {
Expand Down Expand Up @@ -556,4 +565,20 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM
complete(resourceService.deletePolicy(policyId, samRequestContext).map(_ => StatusCodes.NoContent))
}
}

def filterUserResources(samUser: SamUser, samRequestContext: SamRequestContext): Route =
parameters("resourceTypes".as[String].?, "policies".as[String].?, "roles".as[String].?, "actions".as[String].?, "includePublic" ? false) {
(resourceTypes: Option[String], policies: Option[String], roles: Option[String], actions: Option[String], includePublic: Boolean) =>
complete(
resourceService.filterResources(
samUser,
resourceTypes.map(_.split(",").map(ResourceTypeName(_)).toSet).getOrElse(Set.empty),
policies.map(_.split(",").map(AccessPolicyName(_)).toSet).getOrElse(Set.empty),
roles.map(_.split(",").map(ResourceRoleName(_)).toSet).getOrElse(Set.empty),
actions.map(_.split(",").map(ResourceAction(_)).toSet).getOrElse(Set.empty),
includePublic,
samRequestContext
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.data.NonEmptyList
import cats.effect.IO
import org.broadinstitute.dsde.workbench.model._
import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipResponse, SamUser}
import org.broadinstitute.dsde.workbench.sam.model.{FullyQualifiedResourceId, _}
import org.broadinstitute.dsde.workbench.sam.model._
import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext

/** Created by dvoet on 6/26/17.
Expand Down Expand Up @@ -107,6 +107,16 @@ trait AccessPolicyDAO {
ResourceIdWithRolesAndActions(resourceId, left.direct ++ right.direct, left.inherited ++ right.inherited, left.public ++ right.public)
}
}
def filterResources(
samUser: SamUser,
resourceTypeNames: Set[ResourceTypeName],
policies: Set[AccessPolicyName],
roles: Set[ResourceRoleName],
actions: Set[ResourceAction],
includePublic: Boolean,
samRequestContext: SamRequestContext
): IO[Seq[FilterResourcesResult]]

}

sealed abstract class LoadResourceAuthDomainResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,118 @@ class PostgresAccessPolicyDAO(
.toSet
}
}
override def filterResources(
samUser: SamUser,
resourceTypeNames: Set[ResourceTypeName],
policies: Set[AccessPolicyName],
roles: Set[ResourceRoleName],
actions: Set[ResourceAction],
includePublic: Boolean,
samRequestContext: SamRequestContext
): IO[Seq[FilterResourcesResult]] = {
val groupMemberFlat = GroupMemberFlatTable.syntax("groupMemberFlat")
val resourcePolicy = PolicyTable.syntax("resourcePolicy")
val effectiveResourcePolicy = EffectiveResourcePolicyTable.syntax("effectiveResourcePolicy")
val effectivePolicyRole = EffectivePolicyRoleTable.syntax("effectivePolicyRole")
val effectivePolicyAction = EffectivePolicyActionTable.syntax("effectivePolicyAction")
val resourceRole = ResourceRoleTable.syntax("resourceRole")
val roleAction = RoleActionTable.syntax("roleAction")
val resourceAction = ResourceActionTable.syntax("resourceAction")
val resource = ResourceTable.syntax("resource")

val resourceTypePKsToName: Map[ResourceTypePK, ResourceTypeName] =
resourceTypeNames.flatMap(name => resourceTypePKsByName.get(name).map(pk => pk -> name)).toMap

val resourceTypeConstraint =
if (resourceTypeNames.nonEmpty) samsqls"and ${resource.resourceTypeId} in (${resourceTypeNames.flatMap(resourceTypePKsByName.get).toSet})"
else samsqls""
val policyConstraint = if (policies.nonEmpty) samsqls"and ${resourcePolicy.name} in (${policies.toSet})" else samsqls""
val roleConstraint = if (roles.nonEmpty) samsqls"and ${resourceRole.role} in (${roles.toSet})" else samsqls""
val actionConstraint = if (actions.nonEmpty) samsqls"and ${resourceAction.action} in (${actions.toSet})" else samsqls""

val policyRoleActionQuery =
samsqls"""
select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, ${resourceRole.result.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}
from ${GroupMemberFlatTable as groupMemberFlat}
left join ${PolicyTable as resourcePolicy} on ${groupMemberFlat.groupId} = ${resourcePolicy.groupId}
left join ${EffectiveResourcePolicyTable as effectiveResourcePolicy} on ${resourcePolicy.id} = ${effectiveResourcePolicy.sourcePolicyId}
left join ${EffectivePolicyRoleTable as effectivePolicyRole} on ${effectiveResourcePolicy.id} = ${effectivePolicyRole.effectiveResourcePolicyId}
left join ${ResourceRoleTable as resourceRole} on ${effectivePolicyRole.resourceRoleId} = ${resourceRole.id}
left join ${RoleActionTable as roleAction} on ${effectivePolicyRole.resourceRoleId} = ${roleAction.resourceRoleId}
left join ${ResourceActionTable as resourceAction} on ${roleAction.resourceActionId} = ${resourceAction.id}
left join ${ResourceTable as resource} on ${effectiveResourcePolicy.resourceId} = ${resource.id}
where ${groupMemberFlat.memberUserId} = ${samUser.id}
$resourceTypeConstraint
$policyConstraint
$roleConstraint
$actionConstraint"""
val policyActionQuery =
samsqls"""
select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, null as ${resourceRole.resultName.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}
from ${GroupMemberFlatTable as groupMemberFlat}
left join ${PolicyTable as resourcePolicy} on ${groupMemberFlat.groupId} = ${resourcePolicy.groupId}
left join ${EffectiveResourcePolicyTable as effectiveResourcePolicy} on ${resourcePolicy.id} = ${effectiveResourcePolicy.sourcePolicyId}
left join ${EffectivePolicyActionTable as effectivePolicyAction} on ${effectiveResourcePolicy.id} = ${effectivePolicyAction.effectiveResourcePolicyId}
left join ${ResourceActionTable as resourceAction} on ${effectivePolicyAction.resourceActionId} = ${resourceAction.id}
left join ${ResourceTable as resource} on ${effectiveResourcePolicy.resourceId} = ${resource.id}
where ${groupMemberFlat.memberUserId} = ${samUser.id}
$resourceTypeConstraint
$policyConstraint
$actionConstraint"""

val publicRoleActionQuery =
samsqls"""
select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, ${resourceRole.result.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}
from ${PolicyTable as resourcePolicy}
left join ${EffectiveResourcePolicyTable as effectiveResourcePolicy} on ${resourcePolicy.id} = ${effectiveResourcePolicy.sourcePolicyId} and ${resourcePolicy.public}
left join ${EffectivePolicyRoleTable as effectivePolicyRole} on ${effectiveResourcePolicy.id} = ${effectivePolicyRole.effectiveResourcePolicyId}
left join ${ResourceRoleTable as resourceRole} on ${effectivePolicyRole.resourceRoleId} = ${resourceRole.id}
left join ${RoleActionTable as roleAction} on ${effectivePolicyRole.resourceRoleId} = ${roleAction.resourceRoleId}
left join ${ResourceActionTable as resourceAction} on ${roleAction.resourceActionId} = ${resourceAction.id}
left join ${ResourceTable as resource} on ${effectiveResourcePolicy.resourceId} = ${resource.id} $resourceTypeConstraint
where ${resourcePolicy.public}
$resourceTypeConstraint
$policyConstraint
$roleConstraint
$actionConstraint"""
val publicPolicyActionQuery =
samsqls"""
select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, null as ${resourceRole.resultName.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}
from ${PolicyTable as resourcePolicy}
left join ${EffectiveResourcePolicyTable as effectiveResourcePolicy} on ${resourcePolicy.id} = ${effectiveResourcePolicy.sourcePolicyId} and ${resourcePolicy.public}
left join ${EffectivePolicyActionTable as effectivePolicyAction} on ${effectiveResourcePolicy.id} = ${effectivePolicyAction.effectiveResourcePolicyId}
left join ${ResourceActionTable as resourceAction} on ${effectivePolicyAction.resourceActionId} = ${resourceAction.id}
left join ${ResourceTable as resource} on ${effectiveResourcePolicy.resourceId} = ${resource.id} $resourceTypeConstraint
where ${resourcePolicy.public}
$resourceTypeConstraint
$policyConstraint
$actionConstraint"""

val includePolicyActionQuery = if (policies.nonEmpty || actions.nonEmpty) samsqls"union $policyActionQuery" else samsqls""
val includePublicPolicyActionQuery = if ((policies.nonEmpty || actions.nonEmpty) && includePublic) samsqls"union $publicPolicyActionQuery" else samsqls""
val includePublicQuery = if (includePublic) samsqls"union $publicRoleActionQuery $includePublicPolicyActionQuery" else samsqls""

val query =
samsqls"""$policyRoleActionQuery
$includePolicyActionQuery
$includePublicQuery"""

readOnlyTransaction("filterResources", samRequestContext) { implicit session =>
samsql"$query"
.map(rs =>
FilterResourcesResult(
rs.get[ResourceId](resource.resultName.name),
resourceTypePKsToName(rs.get[ResourceTypePK](resource.resultName.resourceTypeId)),
rs.stringOpt(resourcePolicy.resultName.name).map(AccessPolicyName(_)),
rs.stringOpt(resourceRole.resultName.role).map(ResourceRoleName(_)),
rs.stringOpt(resourceAction.resultName.action).map(ResourceAction(_)),
rs.get[Boolean](resourcePolicy.resultName.public)
)
)
.list()
.apply()
}
}

private def recreateEffectivePolicyRolesTableEntry(resourceTypeNames: Set[ResourceTypeName])(implicit session: DBSession): Int = {
val resource = ResourceTable.syntax("resource")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.broadinstitute.dsde.workbench.sam.model

case class FilterResourcesResult(
resourceId: ResourceId,
resourceTypeName: ResourceTypeName,
policy: Option[AccessPolicyName],
role: Option[ResourceRoleName],
action: Option[ResourceAction],
isPublic: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.broadinstitute.dsde.workbench.sam.model.api

import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicyName, ResourceAction, ResourceId, ResourceRoleName, ResourceTypeName}
import spray.json.DefaultJsonProtocol.jsonFormat1
import spray.json.RootJsonFormat
import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._
import spray.json.DefaultJsonProtocol._

object FilteredResources {
implicit val FilteredResourcesFormat: RootJsonFormat[FilteredResources] = jsonFormat1(FilteredResources.apply)

}
case class FilteredResources(resources: Set[FilteredResource])

object FilteredResource {
implicit val FilteredResourceFormat: RootJsonFormat[FilteredResource] = jsonFormat6(FilteredResource.apply)

}
case class FilteredResource(
resourceType: ResourceTypeName,
resourceId: ResourceId,
policies: Set[AccessPolicyName],
roles: Set[ResourceRoleName],
actions: Set[ResourceAction],
isPublic: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import org.broadinstitute.dsde.workbench.sam.audit.SamAuditModelJsonSupport._
import org.broadinstitute.dsde.workbench.sam.audit._
import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO, LoadResourceAuthDomainResult}
import org.broadinstitute.dsde.workbench.sam.model._
import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AccessPolicyMembershipResponse, SamUser}
import org.broadinstitute.dsde.workbench.sam.model.api.{
AccessPolicyMembershipRequest,
AccessPolicyMembershipResponse,
FilteredResource,
FilteredResources,
SamUser
}
import org.broadinstitute.dsde.workbench.sam.util.{API_TIMING_DURATION_BUCKET, SamRequestContext}

import java.util.UUID
Expand Down Expand Up @@ -911,4 +917,36 @@ class ResourceService(
val addEventSet = if (addEvent.changeDetails.isEmpty) Set.empty else Set(addEvent)
addEventSet ++ removeEventSet
}

def filterResources(
samUser: SamUser,
resourceTypeNames: Set[ResourceTypeName],
policies: Set[AccessPolicyName],
roles: Set[ResourceRoleName],
actions: Set[ResourceAction],
includePublic: Boolean,
samRequestContext: SamRequestContext
): IO[FilteredResources] = {
val filterResourcesResult = accessPolicyDAO.filterResources(samUser, resourceTypeNames, policies, roles, actions, includePublic, samRequestContext)

for {
dbResult <- filterResourcesResult
} yield {
val groupedFilteredResource = dbResult
.groupBy(_.resourceId)
.map { tuple =>
val (k, v) = tuple
FilteredResource(
resourceId = k,
resourceType = v.map(_.resourceTypeName).head,
policies = v.flatMap(_.policy).toSet,
roles = v.flatMap(_.role).toSet,
actions = v.flatMap(_.action).toSet,
isPublic = v.map(_.isPublic).head
)
}
.toSet
FilteredResources(resources = groupedFilteredResource)
}
}
}
Loading
Loading