diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala index 2a4569ebd..61305c8bb 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala @@ -259,7 +259,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM def getUserResourcesOfType(resourceType: ResourceType, samUser: SamUser, samRequestContext: SamRequestContext): server.Route = get { - complete(policyEvaluatorService.listUserResources(resourceType.name, samUser.id, samRequestContext)) + complete(resourceService.listUserResources(resourceType.name, samUser.id, samRequestContext)) } def postResource(resourceType: ResourceType, samUser: SamUser, samRequestContext: SamRequestContext): server.Route = @@ -590,7 +590,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM complete { resourceService .listResourcesFlat( - samUser, + samUser.id, 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), @@ -604,7 +604,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM complete { resourceService .listResourcesHierarchical( - samUser, + samUser.id, 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), diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala index 5fcd03cca..ae34fa509 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala @@ -108,7 +108,7 @@ trait AccessPolicyDAO { } } def filterResources( - samUser: SamUser, + samUserId: WorkbenchUserId, resourceTypeNames: Set[ResourceTypeName], policies: Set[AccessPolicyName], roles: Set[ResourceRoleName], diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAO.scala index 9673c29e4..c9e77d943 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAO.scala @@ -22,6 +22,10 @@ import scalikejdbc._ import scala.collection.concurrent.TrieMap import scala.util.{Failure, Try} import cats.effect.Temporal +import org.apache.commons.collections4.map.PassiveExpiringMap + +import java.util.Collections +import java.util.concurrent.TimeUnit class PostgresAccessPolicyDAO( protected val writeDbRef: DbReference, @@ -1686,13 +1690,86 @@ class PostgresAccessPolicyDAO( .toSet } } - override def filterResources( - samUser: SamUser, + + private val publicResourcesCache: java.util.Map[ResourceTypeName, Seq[FilterResourcesResult]] = + Collections.synchronizedMap(new PassiveExpiringMap(1, TimeUnit.HOURS)) + + private def getPublicResourcesOfType(resourceTypeName: ResourceTypeName, samRequestContext: SamRequestContext): IO[Seq[FilterResourcesResult]] = { + 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 resourceTypeConstraint = + samsqls"and ${resource.resourceTypeId} = ${resourceTypePKsByName.get(resourceTypeName)}" + val notNullConstraintRoleAction = + samsqls"and not (${resourceRole.role} is null and ${resourceAction.action} is null)" + val notNullConstraintPolicyAction = samsqls"and not (${resourceAction.action} is null)" + + val publicRoleActionQuery = + samsqls""" + select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, ${resourceRole.result.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}, ${resourcePolicy.resourceId} != ${resource.id} as inherited + 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 + $notNullConstraintRoleAction + """ + + val publicPolicyActionQuery = + samsqls""" + select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, null as ${resourceRole.resultName.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}, ${resourcePolicy.resourceId} != ${resource.id} as inherited + 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 + $notNullConstraintPolicyAction + """ + + val includePublicPolicyActionQuery = samsqls"union $publicPolicyActionQuery" + val publicResourcesQuery = samsql"$publicRoleActionQuery $includePublicPolicyActionQuery" + + readOnlyTransaction("filterResourcesPublic", samRequestContext) { implicit session => + publicResourcesCache.computeIfAbsent( + resourceTypeName, + resourceTypeName => + publicResourcesQuery + .map(rs => + FilterResourcesResult( + rs.get[ResourceId](resource.resultName.name), + resourceTypeNamesByPK(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), + None, + false, + rs.booleanOpt("inherited").getOrElse(false) + ) + ) + .list() + .apply() + ) + } + } + private def filterPrivateResources( + samUserId: WorkbenchUserId, resourceTypeNames: Set[ResourceTypeName], policies: Set[AccessPolicyName], roles: Set[ResourceRoleName], actions: Set[ResourceAction], - includePublic: Boolean, samRequestContext: SamRequestContext ): IO[Seq[FilterResourcesResult]] = { val groupMemberFlat = GroupMemberFlatTable.syntax("groupMemberFlat") @@ -1714,6 +1791,9 @@ class PostgresAccessPolicyDAO( val policyConstraint = if (policies.nonEmpty) samsqls"and ${resourcePolicy.name} in (${policies})" else samsqls"" val roleConstraint = if (roles.nonEmpty) samsqls"and ${resourceRole.role} in (${roles})" else samsqls"" val actionConstraint = if (actions.nonEmpty) samsqls"and ${resourceAction.action} in (${actions})" else samsqls"" + val notNullConstraintRoleAction = + samsqls"and not (${resourceRole.role} is null and ${resourceAction.action} is null)" + val notNullConstraintPolicyAction = samsqls"and not (${resourceAction.action} is null)" val policyRoleActionQuery = samsqls""" @@ -1728,12 +1808,15 @@ class PostgresAccessPolicyDAO( left join ${ResourceTable as resource} on ${effectiveResourcePolicy.resourceId} = ${resource.id} left join ${AuthDomainTable as authDomain} on ${authDomain.resourceId} = ${resource.id} left join ${GroupTable as authDomainGroup} on ${authDomainGroup.id} = ${authDomain.groupId} - left join ${GroupMemberFlatTable as authDomainGroupMemberFlat} on ${authDomainGroup.id} = ${authDomainGroupMemberFlat.groupId} and ${authDomainGroupMemberFlat.memberUserId} = ${samUser.id} - where ${groupMemberFlat.memberUserId} = ${samUser.id} + left join ${GroupMemberFlatTable as authDomainGroupMemberFlat} on ${authDomainGroup.id} = ${authDomainGroupMemberFlat.groupId} and ${authDomainGroupMemberFlat.memberUserId} = ${samUserId} + where ${groupMemberFlat.memberUserId} = ${samUserId} $resourceTypeConstraint $policyConstraint $roleConstraint - $actionConstraint""" + $actionConstraint + $notNullConstraintRoleAction + """ + val policyActionQuery = samsqls""" select ${resource.result.name}, ${resource.result.resourceTypeId}, ${resourcePolicy.result.name}, null as ${resourceRole.resultName.role}, ${resourceAction.result.action}, ${resourcePolicy.result.public}, ${authDomainGroup.result.name}, ${authDomainGroupMemberFlat.memberUserId} is not null as in_auth_domain, ${resourcePolicy.resourceId} != ${resource.id} as inherited @@ -1745,48 +1828,18 @@ class PostgresAccessPolicyDAO( left join ${ResourceTable as resource} on ${effectiveResourcePolicy.resourceId} = ${resource.id} left join ${AuthDomainTable as authDomain} on ${authDomain.resourceId} = ${resource.id} left join ${GroupTable as authDomainGroup} on ${authDomainGroup.id} = ${authDomain.groupId} - left join ${GroupMemberFlatTable as authDomainGroupMemberFlat} on ${authDomainGroup.id} = ${authDomainGroupMemberFlat.groupId} and ${authDomainGroupMemberFlat.memberUserId} = ${samUser.id} - where ${groupMemberFlat.memberUserId} = ${samUser.id} + left join ${GroupMemberFlatTable as authDomainGroupMemberFlat} on ${authDomainGroup.id} = ${authDomainGroupMemberFlat.groupId} and ${authDomainGroupMemberFlat.memberUserId} = ${samUserId} + where ${groupMemberFlat.memberUserId} = ${samUserId} $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}, null as ${authDomainGroup.resultName.name}, null as in_auth_domain, ${resourcePolicy.resourceId} != ${resource.id} as inherited - 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}, null as ${authDomainGroup.resultName.name}, null as in_auth_domain, ${resourcePolicy.resourceId} != ${resource.id} as inherited - 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"" + $actionConstraint + $notNullConstraintPolicyAction + """ + val includePolicyActionQuery = if (roles.isEmpty) samsqls"union $policyActionQuery" else samsqls"" val query = samsqls"""$policyRoleActionQuery - $includePolicyActionQuery - $includePublicQuery""" + $includePolicyActionQuery""" readOnlyTransaction("filterResources", samRequestContext) { implicit session => samsql"$query" @@ -1808,6 +1861,29 @@ class PostgresAccessPolicyDAO( } } + override def filterResources( + samUserId: WorkbenchUserId, + resourceTypeNames: Set[ResourceTypeName], + policies: Set[AccessPolicyName], + roles: Set[ResourceRoleName], + actions: Set[ResourceAction], + includePublic: Boolean, + samRequestContext: SamRequestContext + ): IO[Seq[FilterResourcesResult]] = + for { + publicResources <- + if (includePublic) { + (if (resourceTypeNames.isEmpty) resourceTypePKsByName.keys.toList else resourceTypeNames.toList) + .map(resourceTypeName => getPublicResourcesOfType(resourceTypeName, samRequestContext)) + .sequence + .map(_.flatten) + } else IO.pure(List.empty) + privateResources <- filterPrivateResources(samUserId, resourceTypeNames, policies, roles, actions, samRequestContext) + } yield publicResources + .filter(r => policies.isEmpty || r.policy.exists(p => policies.contains(p))) + .filter(r => roles.isEmpty || r.role.exists(role => roles.contains(role))) + .filter(r => actions.isEmpty || r.action.exists(action => actions.contains(action))) ++ privateResources + private def recreateEffectivePolicyRolesTableEntry(resourceTypeNames: Set[ResourceTypeName])(implicit session: DBSession): Int = { val resource = ResourceTable.syntax("resource") val policyResource = ResourceTable.syntax("policyResource") diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala index 5123f17ae..4a58bc621 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala @@ -288,6 +288,11 @@ class ResourceService( for { resourceType <- getResourceType(resource.resourceTypeName) _ <- validateAuthDomain(resourceType.get, authDomains, userId, samRequestContext) + accessPolicies <- accessPolicyDAO.listAccessPolicies(resource, samRequestContext) + _ <- + if (accessPolicies.exists(_.public)) { + IO.raiseError(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.BadRequest, "Cannot add an auth domain group to a public resource"))) + } else IO.unit policies <- listResourcePolicies(resource, samRequestContext) _ <- accessPolicyDAO.addResourceAuthDomain(resource, authDomains, samRequestContext) _ <- cloudExtensions.onGroupUpdate(policies.map(p => FullyQualifiedPolicyId(resource, p.policyName)), samRequestContext) @@ -963,8 +968,34 @@ class ResourceService( FilteredResourcesHierarchical(resources = groupedFilteredResources) } + private def toUserResourcesResponse(hierarchicalResource: FilteredResourceHierarchical): UserResourcesResponse = { + val directPolicies = hierarchicalResource.policies.filter(p => !p.inherited) + val inheritedPolicies = hierarchicalResource.policies.filter(p => p.inherited) + val publicPolicies = hierarchicalResource.policies.filter(p => p.isPublic) + + def policiesToRolesAndActions(policies: Set[FilteredResourceHierarchicalPolicy]) = + RolesAndActions(policies.flatMap(_.roles.map(_.role)), policies.flatMap(_.actions)) + + UserResourcesResponse( + hierarchicalResource.resourceId, + policiesToRolesAndActions(directPolicies), + policiesToRolesAndActions(inheritedPolicies), + policiesToRolesAndActions(publicPolicies), + hierarchicalResource.authDomainGroups, + hierarchicalResource.missingAuthDomainGroups + ) + } + def listUserResources( + resourceTypeName: ResourceTypeName, + userId: WorkbenchUserId, + samRequestContext: SamRequestContext + ): IO[Iterable[UserResourcesResponse]] = + for { + resources <- listResourcesHierarchical(userId, Set(resourceTypeName), Set.empty, Set.empty, Set.empty, true, samRequestContext) + } yield resources.resources.map(toUserResourcesResponse) + def listResourcesFlat( - samUser: SamUser, + samUserId: WorkbenchUserId, resourceTypeNames: Set[ResourceTypeName], policies: Set[AccessPolicyName], roles: Set[ResourceRoleName], @@ -972,10 +1003,10 @@ class ResourceService( includePublic: Boolean, samRequestContext: SamRequestContext ): IO[FilteredResourcesFlat] = - accessPolicyDAO.filterResources(samUser, resourceTypeNames, policies, roles, actions, includePublic, samRequestContext).map(groupFlat) + accessPolicyDAO.filterResources(samUserId, resourceTypeNames, policies, roles, actions, includePublic, samRequestContext).map(groupFlat) def listResourcesHierarchical( - samUser: SamUser, + samUserId: WorkbenchUserId, resourceTypeNames: Set[ResourceTypeName], policies: Set[AccessPolicyName], roles: Set[ResourceRoleName], @@ -983,5 +1014,7 @@ class ResourceService( includePublic: Boolean, samRequestContext: SamRequestContext ): IO[FilteredResourcesHierarchical] = - accessPolicyDAO.filterResources(samUser, resourceTypeNames, policies, roles, actions, includePublic, samRequestContext).map(groupHierarchical) + accessPolicyDAO + .filterResources(samUserId, resourceTypeNames, policies, roles, actions, includePublic, samRequestContext) + .map(groupHierarchical) } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala index 16ae2da87..04f44ae44 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala @@ -229,6 +229,9 @@ object TestSupport extends TestSupport { PolicyRoleTable, PolicyTable, AuthDomainTable, + EffectiveResourcePolicyTable, + EffectivePolicyRoleTable, + EffectivePolicyActionTable, ResourceTable, RoleActionTable, ResourceActionTable, diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/NewResourceRoutesV2Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/NewResourceRoutesV2Spec.scala index bc56b5892..3803cbaba 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/NewResourceRoutesV2Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/NewResourceRoutesV2Spec.scala @@ -3,7 +3,7 @@ package org.broadinstitute.dsde.workbench.sam.api import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.testkit.ScalatestRouteTest import cats.effect.IO -import org.broadinstitute.dsde.workbench.model.WorkbenchEmail +import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchUserId} import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service._ @@ -31,7 +31,7 @@ class NewResourceRoutesV2Spec extends AnyFlatSpec with Matchers with ScalatestRo .build when( samRoutes.resourceService.listResourcesFlat( - any[SamUser], + any[WorkbenchUserId], any[Set[ResourceTypeName]], any[Set[AccessPolicyName]], any[Set[ResourceRoleName]], @@ -58,7 +58,7 @@ class NewResourceRoutesV2Spec extends AnyFlatSpec with Matchers with ScalatestRo ) when( samRoutes.resourceService.listResourcesHierarchical( - any[SamUser], + any[WorkbenchUserId], any[Set[ResourceTypeName]], any[Set[AccessPolicyName]], any[Set[ResourceRoleName]], @@ -91,7 +91,7 @@ class NewResourceRoutesV2Spec extends AnyFlatSpec with Matchers with ScalatestRo status shouldEqual StatusCodes.OK } verify(samRoutes.resourceService).listResourcesFlat( - any[SamUser], + any[WorkbenchUserId], eqTo(Set.empty), eqTo(Set.empty), eqTo(Set.empty), @@ -106,7 +106,7 @@ class NewResourceRoutesV2Spec extends AnyFlatSpec with Matchers with ScalatestRo status shouldEqual StatusCodes.OK } verify(samRoutes.resourceService).listResourcesFlat( - any[SamUser], + any[WorkbenchUserId], eqTo(Set(ResourceTypeName("fooType"), ResourceTypeName("barType"))), eqTo(Set(AccessPolicyName("fooPolicy"))), eqTo(Set(ResourceRoleName("fooRole"), ResourceRoleName("barRole"), ResourceRoleName("bazRole"))), @@ -121,7 +121,7 @@ class NewResourceRoutesV2Spec extends AnyFlatSpec with Matchers with ScalatestRo status shouldEqual StatusCodes.OK } verify(samRoutes.resourceService).listResourcesHierarchical( - any[SamUser], + any[WorkbenchUserId], eqTo(Set(ResourceTypeName("fooType"), ResourceTypeName("barType"))), eqTo(Set(AccessPolicyName("fooPolicy"))), eqTo(Set(ResourceRoleName("fooRole"), ResourceRoleName("barRole"), ResourceRoleName("bazRole"))), diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala index f2f594169..faff6f82e 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala @@ -927,7 +927,8 @@ class ResourceRoutesV2Spec extends RetryableAnyFlatSpec with Matchers with TestS // Read the policies Get(s"/api/resources/v2/${resourceType.name}") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - responseAs[List[UserResourcesResponse]].size should equal(1) + val response = responseAs[List[UserResourcesResponse]] + response.size should equal(1) } } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockAccessPolicyDAO.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockAccessPolicyDAO.scala index e0e1680f0..f4455f06c 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockAccessPolicyDAO.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockAccessPolicyDAO.scala @@ -363,14 +363,55 @@ class MockAccessPolicyDAO(private val resourceTypes: mutable.Map[ResourceTypeNam } override def filterResources( - samUser: SamUser, + samUserId: WorkbenchUserId, resourceTypeNames: Set[ResourceTypeName], policies: Set[AccessPolicyName], roles: Set[ResourceRoleName], actions: Set[ResourceAction], includePublic: Boolean, samRequestContext: SamRequestContext - ): IO[Seq[FilterResourcesResult]] = - IO.pure(Seq.empty) + ): IO[Seq[FilterResourcesResult]] = IO { + resourceTypeNames + .flatMap { resourceTypeName => + this.policies.collect { + case (fqPolicyId @ FullyQualifiedPolicyId(FullyQualifiedResourceId(`resourceTypeName`, _), _), accessPolicy: AccessPolicy) + if accessPolicy.members.contains(samUserId) || accessPolicy.public => + val rolesAndActions = RolesAndActions.fromPolicy(accessPolicy) + rolesAndActions.roles.flatMap { role => + if (actions.isEmpty) { + Set( + FilterResourcesResult( + fqPolicyId.resource.resourceId, + fqPolicyId.resource.resourceTypeName, + Some(fqPolicyId.accessPolicyName), + Some(role), + None, + accessPolicy.public, + None, + false, + false + ) + ) + } else { + rolesAndActions.actions.map { action => + FilterResourcesResult( + fqPolicyId.resource.resourceId, + fqPolicyId.resource.resourceTypeName, + Some(fqPolicyId.accessPolicyName), + Some(role), + Some(action), + accessPolicy.public, + None, + false, + false + ) + } + } + } + } + } + .flatten + .toSeq + } } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAOSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAOSpec.scala index 9b6ee9b7f..635d2942d 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAOSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresAccessPolicyDAOSpec.scala @@ -32,7 +32,7 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA "PostgresAccessPolicyDAO" - { val resourceTypeName = ResourceTypeName("awesomeType") - val otherRsourceTypeName = ResourceTypeName("lessAwesomeType") + val otherResourceTypeName = ResourceTypeName("lessAwesomeType") val actionPatterns = Set(ResourceActionPattern("write", "description of pattern1", false), ResourceActionPattern("read", "description of pattern2", false)) @@ -47,7 +47,7 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA val roles = Set(ownerRole, readerRole, actionlessRole) val resourceType = ResourceType(resourceTypeName, actionPatterns, roles, ownerRoleName, false) - val otherResourceType = ResourceType(otherRsourceTypeName, actionPatterns, roles, ownerRoleName, false) + val otherResourceType = ResourceType(otherResourceTypeName, actionPatterns, roles, ownerRoleName, false) "upsertResourceTypes" - { "creates resource types in config and is idempotent" in { @@ -3106,6 +3106,22 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA } "filterResources" - { + + def verify( + dbResultRows: Seq[FilterResourcesResult], + roles: Set[ResourceRoleName], + roleActions: Set[ResourceAction], + policyActions: Set[ResourceAction] + ) = { + val testRoles: Set[ResourceRoleName] = dbResultRows.flatMap(_.role).toSet + val testRoleActions: Set[ResourceAction] = dbResultRows.filter(_.role.isDefined).flatMap(_.action).toSet + val testPolicyActions: Set[ResourceAction] = dbResultRows.filter(_.role.isEmpty).flatMap(_.action).toSet + + testRoles should be(roles) + testRoleActions should be(roleActions) + testPolicyActions should be(policyActions) + } + "filters the user's resources by policy, action, and role" in { assume(databaseEnabled, databaseEnabledClue) @@ -3123,9 +3139,9 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA dao.createResourceType(otherResourceType, samRequestContext).unsafeRunSync() // 1 reader role, 1 write action on policy - createResource(Option(user.id), Set(writeAction), Set(readerRole.roleName), false) + val userReadRoleWriteAction = createResource(Option(user.id), Set(writeAction), Set(readerRole.roleName), false) // 1 reader role, 1 write action on policy - createResource(Option(parentGroup.id), Set(writeAction), Set(readerRole.roleName), false) + val groupReadRoleWriteAction = createResource(Option(parentGroup.id), Set(writeAction), Set(readerRole.roleName), false) // 1 reader role, 1 write action on policy val childResource1 = createResourceHierarchy(Option(user.id), Set(writeAction), Set(readerRole.roleName), false) // 1 reader role, 1 write action on policy @@ -3161,7 +3177,8 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA dao.createPolicy(directProbePolicy, samRequestContext).unsafeRunSync() dao.createPolicy(publicProbePolicy, samRequestContext).unsafeRunSync() - val writeActions = dao.filterResources(user, Set(resourceType.name), Set.empty, Set.empty, Set(writeAction), false, samRequestContext).unsafeRunSync() + val writeActions = + dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set.empty, Set(writeAction), false, samRequestContext).unsafeRunSync() writeActions.length should be(5) writeActions.map(_.action).forall(a => a.exists(_.equals(writeAction))) should be(true) writeActions.map(_.isPublic).forall(ip => !ip) should be(true) @@ -3169,42 +3186,63 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA val writerViaOwner = writeActions.filter(r => r.role.exists(_.equals(ownerRole.roleName))) writerViaOwner.size should be(1) - val readActions = dao.filterResources(user, Set(resourceType.name), Set.empty, Set.empty, Set(readAction), false, samRequestContext).unsafeRunSync() + val readActions = dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set.empty, Set(readAction), false, samRequestContext).unsafeRunSync() readActions.length should be(6) val readerViaOwner = readActions.filter(r => r.role.exists(_.equals(ownerRole.roleName))) readerViaOwner.size should be(1) val readerRoles = - dao.filterResources(user, Set(resourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, false, samRequestContext).unsafeRunSync() + dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, false, samRequestContext).unsafeRunSync() readerRoles.size should be(5) val policies = dao - .filterResources(user, Set(resourceTypeName), Set(directProbePolicy.id.accessPolicyName), Set.empty, Set.empty, false, samRequestContext) + .filterResources(user.id, Set(resourceTypeName), Set(directProbePolicy.id.accessPolicyName), Set.empty, Set.empty, false, samRequestContext) .unsafeRunSync() val foundPolicies = policies.flatMap(_.policy).toSet foundPolicies.size should be(1) foundPolicies.head should be(directProbePolicy.id.accessPolicyName) val writeActionsIncludingPublic = - dao.filterResources(user, Set(resourceType.name), Set.empty, Set.empty, Set(writeAction), true, samRequestContext).unsafeRunSync() + dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set.empty, Set(writeAction), true, samRequestContext).unsafeRunSync() writeActionsIncludingPublic.length should be(7) writeActionsIncludingPublic.filter(_.isPublic).map(_.resourceId).toSet should be(Set(publicResource.resourceId, publicChildResource.resourceId)) val readerRolesIncludingPublic = - dao.filterResources(user, Set(resourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, true, samRequestContext).unsafeRunSync() + dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, true, samRequestContext).unsafeRunSync() readerRolesIncludingPublic.size should be(7) readerRolesIncludingPublic.filter(_.isPublic).map(_.resourceId).toSet should be(Set(publicResource.resourceId, publicChildResource.resourceId)) - println(dao.filterResources(user, Set(resourceTypeName), Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync()) + println(dao.filterResources(user.id, Set(resourceTypeName), Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync()) val inheritedReaderRoles = - dao.filterResources(user, Set(resourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, true, samRequestContext).unsafeRunSync() + dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, true, samRequestContext).unsafeRunSync() val inheritedPolicies = inheritedReaderRoles.filter(_.inherited) inheritedPolicies.map(_.resourceId).toSet should be( Set(childResource1.resourceId, childResource2.resourceId, publicChildResource.resourceId, kitchenSink.resourceId) ) + + val filtered = dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync() + + val readRoleWriteActionResources = + Seq(userReadRoleWriteAction, groupReadRoleWriteAction, childResource1, childResource2, publicResource, publicChildResource) + + readRoleWriteActionResources + .map(resource => + verify( + filtered.filter(_.resourceId.equals(resource.resourceId)), + roles = Set(readerRole.roleName), + roleActions = Set(readAction), + policyActions = Set(writeAction) + ) + ) + verify( + filtered.filter(_.resourceId.equals(kitchenSink.resourceId)), + roles = Set(readerRole.roleName, ownerRole.roleName, actionlessRole.roleName), + roleActions = Set(readAction, writeAction), + policyActions = Set.empty + ) } "filters on the user's policies, roles, and actions when using nested roles" in { @@ -3241,14 +3279,14 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA createResourceHierarchy(Option(parentGroup.id), Set(writeAction), Set(includesRole.roleName, descendsRole.roleName), false, nestedResourceType.name) val nestedReaderRoles = - dao.filterResources(user, Set(nestedResourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, false, samRequestContext).unsafeRunSync() + dao.filterResources(user.id, Set(nestedResourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, false, samRequestContext).unsafeRunSync() nestedReaderRoles.size should be(4) nestedReaderRoles.map(_.resourceId).toSet should be( Set(directAccessResource.resourceId, groupAccessResource.resourceId, directAccessChildResource.resourceId, groupAccessChildResource.resourceId) ) val publicNestedReaderRoles = - dao.filterResources(user, Set(nestedResourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, true, samRequestContext).unsafeRunSync() + dao.filterResources(user.id, Set(nestedResourceType.name), Set.empty, Set(readerRole.roleName), Set.empty, true, samRequestContext).unsafeRunSync() publicNestedReaderRoles.size should be(6) publicNestedReaderRoles.filter(_.isPublic).map(_.resourceId).toSet should be(Set(publicResource.resourceId, publicChildResource.resourceId)) } @@ -3279,7 +3317,8 @@ class PostgresAccessPolicyDAOSpec extends AnyFreeSpec with Matchers with BeforeA val resource2 = createResource(Option(user.id), Set(writeAction), Set(readerRole.roleName), false, authDomainGroups = Set(authDomainGroup1, authDomainGroup2)) - val writeActions = dao.filterResources(user, Set(resourceType.name), Set.empty, Set.empty, Set(writeAction), false, samRequestContext).unsafeRunSync() + val writeActions = + dao.filterResources(user.id, Set(resourceType.name), Set.empty, Set.empty, Set(writeAction), false, samRequestContext).unsafeRunSync() val byResource = writeActions.groupBy(_.resourceId) val resource1Results = byResource(resource1.resourceId) diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockAccessPolicyDaoBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockAccessPolicyDaoBuilder.scala index 0398b6df6..9acb40e3a 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockAccessPolicyDaoBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockAccessPolicyDaoBuilder.scala @@ -67,6 +67,35 @@ case class StatefulMockAccessPolicyDaoBuilder() extends MockitoSugar { argThat(MatchesOneOf(policy.members.map(m => WorkbenchUserId(m.toString)))), any[SamRequestContext] ) + + // This logic should probably hit a real database. The complexity of mocking this is pretty bad. + lenient() + .doAnswer { (i: InvocationOnMock) => + val workbenchUserId = i.getArgument[WorkbenchUserId](0) + val resourceTypeName = i.getArgument[Set[ResourceTypeName]](1).head + val policies = Map(policy.id -> policy) + + IO { + val forEachPolicy = policies.collect { + case (FullyQualifiedPolicyId(FullyQualifiedResourceId(`resourceTypeName`, _), _), accessPolicy: AccessPolicy) + if accessPolicy.members.contains(workbenchUserId) || accessPolicy.public => + constructFilterResourcesResult(accessPolicy) + } + + forEachPolicy.flatten + } + + } + .when(mockedAccessPolicyDAO) + .filterResources( + argThat(MatchesOneOf(policy.members.map(m => WorkbenchUserId(m.toString)))), + ArgumentMatchers.eq(Set(policy.id.resource.resourceTypeName)), + ArgumentMatchers.eq(Set.empty), + ArgumentMatchers.eq(Set.empty), + ArgumentMatchers.eq(Set.empty), + ArgumentMatchers.eq(true), + any[SamRequestContext] + ) } private def constructResourceIdWithRolesAndActions(accessPolicy: AccessPolicy): ResourceIdWithRolesAndActions = @@ -86,6 +115,68 @@ case class StatefulMockAccessPolicyDaoBuilder() extends MockitoSugar { ) } + private def constructFilterResourcesResult(accessPolicy: AccessPolicy): Seq[FilterResourcesResult] = + if (accessPolicy.roles.isEmpty) { + Seq( + FilterResourcesResult( + accessPolicy.id.resource.resourceId, + accessPolicy.id.resource.resourceTypeName, + Some(accessPolicy.id.accessPolicyName), + None, + None, + accessPolicy.public, + None, + false, + false + ) + ) + } else + { + accessPolicy.roles.map { role => + FilterResourcesResult( + accessPolicy.id.resource.resourceId, + accessPolicy.id.resource.resourceTypeName, + Some(accessPolicy.id.accessPolicyName), + Some(role), + None, + accessPolicy.public, + None, + false, + false + ) + }.toSeq + } ++ + (if (accessPolicy.actions.isEmpty) { + Seq( + FilterResourcesResult( + accessPolicy.id.resource.resourceId, + accessPolicy.id.resource.resourceTypeName, + Some(accessPolicy.id.accessPolicyName), + None, + None, + accessPolicy.public, + None, + false, + false + ) + ) + } else { + accessPolicy.actions.map { action => + FilterResourcesResult( + accessPolicy.id.resource.resourceId, + accessPolicy.id.resource.resourceTypeName, + Some(accessPolicy.id.accessPolicyName), + None, + Some(action), + accessPolicy.public, + None, + false, + false + ) + + }.toSeq + }) + def withRandomAccessPolicy(resourceTypeName: ResourceTypeName, members: Set[WorkbenchSubject]): StatefulMockAccessPolicyDaoBuilder = { val policy = AccessPolicy( FullyQualifiedPolicyId( diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/PolicyEvaluatorServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/PolicyEvaluatorServiceSpec.scala index 121e87942..5fd56526f 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/PolicyEvaluatorServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/PolicyEvaluatorServiceSpec.scala @@ -517,24 +517,28 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with samRequestContext ) r <- service.policyEvaluatorService.listUserResources(defaultResourceType.name, dummyUser.id, samRequestContext) - } yield r should contain theSameElementsAs Set( - UserResourcesResponse( - resource1.resourceId, - RolesAndActions(Set(defaultResourceType.ownerRoleName), Set(ResourceAction("alter_policies"))), - RolesAndActions.empty, - RolesAndActions.empty, - Set.empty, - Set.empty - ), - UserResourcesResponse( - resource2.resourceId, - RolesAndActions.fromRoles(Set(defaultResourceType.ownerRoleName)), - RolesAndActions.empty, - RolesAndActions.empty, - Set.empty, - Set.empty + filtered <- service.listUserResources(defaultResourceType.name, dummyUser.id, samRequestContext) + } yield { + r should contain theSameElementsAs Set( + UserResourcesResponse( + resource1.resourceId, + RolesAndActions(Set(defaultResourceType.ownerRoleName), Set(ResourceAction("alter_policies"))), + RolesAndActions.empty, + RolesAndActions.empty, + Set.empty, + Set.empty + ), + UserResourcesResponse( + resource2.resourceId, + RolesAndActions.fromRoles(Set(defaultResourceType.ownerRoleName)), + RolesAndActions.empty, + RolesAndActions.empty, + Set.empty, + Set.empty + ) ) - ) + r should contain theSameElementsAs filtered + } test.unsafeRunSync() } @@ -566,6 +570,7 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with samRequestContext ) r <- constrainableService.policyEvaluatorService.listUserResources(constrainableResourceType.name, dummyUser.id, samRequestContext) + filtered <- constrainableService.listUserResources(constrainableResourceType.name, dummyUser.id, samRequestContext) } yield { val expected = Set( UserResourcesResponse( @@ -578,6 +583,7 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with ) ) r should contain theSameElementsAs expected + r should contain theSameElementsAs filtered } res.unsafeRunSync() @@ -609,6 +615,8 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with samRequestContext ) r <- constrainableService.policyEvaluatorService.listUserResources(constrainableResourceType.name, dummyUser.id, samRequestContext) + filtered <- constrainableService.listUserResources(constrainableResourceType.name, dummyUser.id, samRequestContext) + } yield { val expected = Set( UserResourcesResponse( @@ -621,6 +629,7 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with ) ) r should contain theSameElementsAs expected + r should contain theSameElementsAs filtered } res.unsafeRunSync() @@ -654,6 +663,7 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with _ <- dirDAO.createUser(user, samRequestContext) _ <- constrainableService.createPolicy(policy.id, policy.members + user.id, policy.roles, policy.actions, Set.empty, samRequestContext) r <- constrainableService.policyEvaluatorService.listUserResources(constrainableResourceType.name, user.id, samRequestContext) + filtered <- constrainableService.listUserResources(constrainableResourceType.name, user.id, samRequestContext) } yield { val expected = Set( UserResourcesResponse( @@ -666,6 +676,7 @@ class PolicyEvaluatorServiceSpec extends RetryableAnyFlatSpec with Matchers with ) ) r should contain theSameElementsAs expected + r should contain theSameElementsAs filtered } res.unsafeRunSync() diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceUnitSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceUnitSpec.scala index 98198e1a5..a4859f427 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceUnitSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceUnitSpec.scala @@ -2,7 +2,7 @@ package org.broadinstitute.dsde.workbench.sam.service import cats.effect.IO import cats.effect.unsafe.implicits.{global => globalEc} -import org.broadinstitute.dsde.workbench.model.WorkbenchGroupName +import org.broadinstitute.dsde.workbench.model.{WorkbenchGroupName, WorkbenchUserId} import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO} import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ @@ -116,7 +116,7 @@ class ResourceServiceUnitSpec extends AnyFlatSpec with Matchers with ScalaFuture val mockAccessPolicyDAO = mock[AccessPolicyDAO] when( mockAccessPolicyDAO.filterResources( - any[SamUser], + any[WorkbenchUserId], any[Set[ResourceTypeName]], any[Set[AccessPolicyName]], any[Set[ResourceRoleName]], @@ -138,7 +138,7 @@ class ResourceServiceUnitSpec extends AnyFlatSpec with Matchers with ScalaFuture ) "ResourceService" should "group filtered resources from the database appropriately flatly" in { - val filteredResources = resourceService.listResourcesFlat(dummyUser, Set.empty, Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync() + val filteredResources = resourceService.listResourcesFlat(dummyUser.id, Set.empty, Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync() val oneResource = filteredResources.resources.filter(_.resourceId.equals(testResourceId)).head oneResource.resourceType should be(resourceTypeName) @@ -166,7 +166,7 @@ class ResourceServiceUnitSpec extends AnyFlatSpec with Matchers with ScalaFuture it should "group filtered resources from the database appropriately hierarchically" in { val filteredResources = - resourceService.listResourcesHierarchical(dummyUser, Set.empty, Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync() + resourceService.listResourcesHierarchical(dummyUser.id, Set.empty, Set.empty, Set.empty, Set.empty, true, samRequestContext).unsafeRunSync() val oneResource = filteredResources.resources.filter(_.resourceId.equals(testResourceId)).head oneResource.resourceType should be(resourceTypeName)