From 798f6cfec8cc3f6cf011b0ea7a08624c5addf762 Mon Sep 17 00:00:00 2001 From: Trevyn Langsford Date: Mon, 29 Jul 2024 12:51:27 -0400 Subject: [PATCH] ID-1341 Auth Domain Constraint Satisfaction Endpoint (#1502) * ID-1341 Auth Domain Constraint Satisfaction Endpoint * change api-docs operationId --------- Co-authored-by: Tristan Garwood --- src/main/resources/swagger/api-docs.yaml | 36 ++++++ .../workbench/sam/api/ResourceRoutes.scala | 9 ++ .../sam/service/ResourceService.scala | 12 ++ .../sam/api/ResourceRoutesV2Spec.scala | 65 ++++++++++ .../sam/service/ResourceServiceSpec.scala | 120 +++++++++++++++++- 5 files changed, 241 insertions(+), 1 deletion(-) diff --git a/src/main/resources/swagger/api-docs.yaml b/src/main/resources/swagger/api-docs.yaml index bd74d445f..e7ef83d59 100755 --- a/src/main/resources/swagger/api-docs.yaml +++ b/src/main/resources/swagger/api-docs.yaml @@ -2106,6 +2106,42 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorReport' + /api/resources/v2/{resourceTypeName}/{resourceId}/authDomain/satisfied: + get: + tags: + - Resources + summary: Check whether the calling user satisfies all Authoriaztion Domain Constraints + operationId: isAuthDomainV2Satisfied + parameters: + - name: resourceTypeName + in: path + description: Type of resource + required: true + schema: + type: string + - name: resourceId + in: path + description: Id of resource + required: true + schema: + type: string + responses: + 200: + description: User satisfies all authorization domain constraints. + Empty if an Auth Domain has not been set + 403: + description: User does not satisfy all authorization domain constraints. + content: { } + 404: + description: Resource type or resource does not exist or you are not a member + of any policy on the resource + content: { } + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' /api/resources/v2/{resourceTypeName}/{resourceId}/allUsers: get: tags: 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 04dc142c5..82cf8e141 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 @@ -168,6 +168,15 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM pathEndOrSingleSlash { getResourceAuthDomain(resource, samUser, samRequestContext) ~ patchResourceAuthDomain(resource, samUser, samRequestContext) + } ~ + pathPrefix("satisfied") { + pathEndOrSingleSlash { + complete { + resourceService.satisfiesAuthDomainConstrains(resource, samUser, samRequestContext).map { satisfied => + if (satisfied) StatusCodes.OK else StatusCodes.Forbidden + } + } + } } } ~ pathPrefix("roles") { 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 bcd2f46c0..b1aa946d7 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 @@ -308,6 +308,18 @@ class ResourceService( } ) + def satisfiesAuthDomainConstrains(resource: FullyQualifiedResourceId, samUser: SamUser, samRequestContext: SamRequestContext): IO[Boolean] = + loadResourceAuthDomain(resource, samRequestContext).flatMap { authDomain => + authDomain.toList.traverse { group => + policyEvaluatorService.hasPermission( + FullyQualifiedResourceId(ManagedGroupService.managedGroupTypeName, ResourceId(group.value)), + ManagedGroupService.useAction, + samUser.id, + samRequestContext + ) + } + } map { listOfUsePermissions => listOfUsePermissions.isEmpty || listOfUsePermissions.forall(identity) } + def addResourceAuthDomain( resource: FullyQualifiedResourceId, authDomains: Set[WorkbenchGroupName], 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 44a653caa..7b594a888 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 @@ -1764,6 +1764,71 @@ class ResourceRoutesV2Spec extends RetryableAnyFlatSpec with Matchers with TestS } } + "GET /api/resources/v2/{resourceType}/{resourceId}/authDomain/satisfied" should "200 if the calling user satisfies the auth domain constraints" in { + val managedGroupResourceType = initManagedGroupResourceType() + + val authDomain = "authDomain" + val resourceType = ResourceType( + ResourceTypeName("rt"), + Set(SamResourceActionPatterns.readAuthDomain, SamResourceActionPatterns.use), + Set(ResourceRole(ResourceRoleName("owner"), Set(SamResourceActions.readAuthDomain, ManagedGroupService.useAction))), + ResourceRoleName("owner") + ) + val samRoutes = TestSamRoutes(Map(resourceType.name -> resourceType, managedGroupResourceType.name -> managedGroupResourceType)) + + runAndWait(samRoutes.managedGroupService.createManagedGroup(ResourceId(authDomain), defaultUserInfo, samRequestContext = samRequestContext)) + + val resourceId = ResourceId("foo") + val policiesMap = Map( + AccessPolicyName("ap") -> AccessPolicyMembershipRequest( + Set(defaultUserInfo.email), + Set(SamResourceActions.readAuthDomain, ManagedGroupService.useAction), + Set(ResourceRoleName("owner")) + ) + ) + runAndWait( + samRoutes.resourceService + .createResource(resourceType, resourceId, policiesMap, Set(WorkbenchGroupName(authDomain)), None, defaultUserInfo.id, samRequestContext) + ) + + Get(s"/api/resources/v2/${resourceType.name}/${resourceId.value}/authDomain/satisfied") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.OK + } + } + + it should "403 if the calling user does not satisfy the auth domain constraints" in { + val managedGroupResourceType = initManagedGroupResourceType() + + val authDomain = "authDomain" + val resourceType = ResourceType( + ResourceTypeName("rt"), + Set(SamResourceActionPatterns.readAuthDomain, SamResourceActionPatterns.use), + Set(ResourceRole(ResourceRoleName("owner"), Set(SamResourceActions.readAuthDomain, ManagedGroupService.useAction))), + ResourceRoleName("owner") + ) + val samRoutes = TestSamRoutes(Map(resourceType.name -> resourceType, managedGroupResourceType.name -> managedGroupResourceType)) + val otherUser = Generator.genWorkbenchUserGoogle.sample.get + runAndWait(samRoutes.userService.createUser(otherUser, samRequestContext)) + runAndWait(samRoutes.managedGroupService.createManagedGroup(ResourceId(authDomain), otherUser, samRequestContext = samRequestContext)) + + val resourceId = ResourceId("foo") + val policiesMap = Map( + AccessPolicyName("ap") -> AccessPolicyMembershipRequest( + Set(defaultUserInfo.email), + Set(SamResourceActions.readAuthDomain, ManagedGroupService.useAction), + Set(ResourceRoleName("owner")) + ) + ) + runAndWait( + samRoutes.resourceService + .createResource(resourceType, resourceId, policiesMap, Set(WorkbenchGroupName(authDomain)), None, otherUser.id, samRequestContext) + ) + + Get(s"/api/resources/v2/${resourceType.name}/${resourceId.value}/authDomain/satisfied") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.Forbidden + } + } + private def initManagedGroupResourceType(): ResourceType = { val accessPolicyNames = Set(ManagedGroupService.adminPolicyName, ManagedGroupService.memberPolicyName, ManagedGroupService.adminNotifierPolicyName) val policyActions: Set[ResourceAction] = diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala index 30f2ba53a..2e97201a1 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceServiceSpec.scala @@ -881,7 +881,7 @@ class ResourceServiceSpec } } - it should "fail when user does not have access to at least 1 of the auth domain groups" in { + it should "fail when user does not have access all of the auth domain groups" in { assume(databaseEnabled, databaseEnabledClue) constrainableResourceType.isAuthDomainConstrainable shouldEqual true @@ -913,6 +913,48 @@ class ResourceServiceSpec } } + it should "say auth domain is satisfied when a user is in all auth domain groups, and not satified when a user isn't" in { + assume(databaseEnabled, databaseEnabledClue) + + constrainableResourceType.isAuthDomainConstrainable shouldEqual true + constrainableService.createResourceType(constrainableResourceType, samRequestContext).unsafeRunSync() + + val bender = Generator.genWorkbenchUserBoth.sample.get + dirDAO.createUser(bender, samRequestContext).unsafeRunSync() + + val fry = Generator.genWorkbenchUserBoth.sample.get + dirDAO.createUser(fry, samRequestContext).unsafeRunSync() + + constrainableService.createResourceType(managedGroupResourceType, samRequestContext).unsafeRunSync() + val managedGroupName1 = "firstGroup" + runAndWait(managedGroupService.createManagedGroup(ResourceId(managedGroupName1), dummyUser, samRequestContext = samRequestContext)) + runAndWait(managedGroupService.addSubjectToPolicy(ResourceId(managedGroupName1), ManagedGroupService.adminPolicyName, bender.id, samRequestContext)) + runAndWait(managedGroupService.addSubjectToPolicy(ResourceId(managedGroupName1), ManagedGroupService.adminPolicyName, fry.id, samRequestContext)) + val managedGroupName2 = "benderIsGreat" + runAndWait(managedGroupService.createManagedGroup(ResourceId(managedGroupName2), bender, samRequestContext = samRequestContext)) + runAndWait(managedGroupService.addSubjectToPolicy(ResourceId(managedGroupName2), ManagedGroupService.adminPolicyName, bender.id, samRequestContext)) + + val authDomain = Set(WorkbenchGroupName(managedGroupName1), WorkbenchGroupName(managedGroupName2)) + val viewPolicyName = AccessPolicyName(constrainableReaderRoleName.value) + val resource = runAndWait( + constrainableService.createResource( + constrainableResourceType, + ResourceId(UUID.randomUUID().toString), + Map(viewPolicyName -> constrainablePolicyMembership), + authDomain, + None, + bender.id, + samRequestContext + ) + ) + + val benderAccess = constrainableService.satisfiesAuthDomainConstrains(resource.fullyQualifiedId, bender, samRequestContext).unsafeRunSync() + val fryAccess = constrainableService.satisfiesAuthDomainConstrains(resource.fullyQualifiedId, fry, samRequestContext).unsafeRunSync() + + benderAccess shouldEqual true + fryAccess shouldEqual false + } + "Loading an auth domain" should "fail when the resource does not exist" in { assume(databaseEnabled, databaseEnabledClue) @@ -924,6 +966,82 @@ class ResourceServiceSpec e.getMessage should include("not found") } + "Checking auth domain satisfaction" should "say auth domain is satisfied when a user is in all auth domain groups, and not satisfied when a user isn't" in { + assume(databaseEnabled, databaseEnabledClue) + + constrainableResourceType.isAuthDomainConstrainable shouldEqual true + constrainableService.createResourceType(constrainableResourceType, samRequestContext).unsafeRunSync() + + val bender = Generator.genWorkbenchUserBoth.sample.get + dirDAO.createUser(bender, samRequestContext).unsafeRunSync() + + val fry = Generator.genWorkbenchUserBoth.sample.get + dirDAO.createUser(fry, samRequestContext).unsafeRunSync() + + constrainableService.createResourceType(managedGroupResourceType, samRequestContext).unsafeRunSync() + val managedGroupName1 = "firstGroup" + runAndWait(managedGroupService.createManagedGroup(ResourceId(managedGroupName1), dummyUser, samRequestContext = samRequestContext)) + runAndWait(managedGroupService.addSubjectToPolicy(ResourceId(managedGroupName1), ManagedGroupService.adminPolicyName, bender.id, samRequestContext)) + runAndWait(managedGroupService.addSubjectToPolicy(ResourceId(managedGroupName1), ManagedGroupService.adminPolicyName, fry.id, samRequestContext)) + val managedGroupName2 = "benderIsGreat" + runAndWait(managedGroupService.createManagedGroup(ResourceId(managedGroupName2), bender, samRequestContext = samRequestContext)) + runAndWait(managedGroupService.addSubjectToPolicy(ResourceId(managedGroupName2), ManagedGroupService.adminPolicyName, bender.id, samRequestContext)) + + val authDomain = Set(WorkbenchGroupName(managedGroupName1), WorkbenchGroupName(managedGroupName2)) + val viewPolicyName = AccessPolicyName(constrainableReaderRoleName.value) + val resource = runAndWait( + constrainableService.createResource( + constrainableResourceType, + ResourceId(UUID.randomUUID().toString), + Map(viewPolicyName -> constrainablePolicyMembership), + authDomain, + None, + bender.id, + samRequestContext + ) + ) + + val benderAccess = constrainableService.satisfiesAuthDomainConstrains(resource.fullyQualifiedId, bender, samRequestContext).unsafeRunSync() + val fryAccess = constrainableService.satisfiesAuthDomainConstrains(resource.fullyQualifiedId, fry, samRequestContext).unsafeRunSync() + + benderAccess shouldEqual true + fryAccess shouldEqual false + } + + it should "say the auth domain is satisfied if there are no auth domain constraints" in { + assume(databaseEnabled, databaseEnabledClue) + + constrainableResourceType.isAuthDomainConstrainable shouldEqual true + constrainableService.createResourceType(constrainableResourceType, samRequestContext).unsafeRunSync() + + val bender = Generator.genWorkbenchUserBoth.sample.get + dirDAO.createUser(bender, samRequestContext).unsafeRunSync() + + val fry = Generator.genWorkbenchUserBoth.sample.get + dirDAO.createUser(fry, samRequestContext).unsafeRunSync() + + constrainableService.createResourceType(managedGroupResourceType, samRequestContext).unsafeRunSync() + + val viewPolicyName = AccessPolicyName(constrainableReaderRoleName.value) + val resource = runAndWait( + constrainableService.createResource( + constrainableResourceType, + ResourceId(UUID.randomUUID().toString), + Map(viewPolicyName -> constrainablePolicyMembership), + Set.empty, + None, + bender.id, + samRequestContext + ) + ) + + val benderAccess = constrainableService.satisfiesAuthDomainConstrains(resource.fullyQualifiedId, bender, samRequestContext).unsafeRunSync() + val fryAccess = constrainableService.satisfiesAuthDomainConstrains(resource.fullyQualifiedId, fry, samRequestContext).unsafeRunSync() + + benderAccess shouldEqual true + fryAccess shouldEqual true + } + "Creating a resource that has 0 constrainable action patterns" should "fail when an auth domain is provided" in { assume(databaseEnabled, databaseEnabledClue)