diff --git a/.scalafmt.conf b/.scalafmt.conf index a792c0af1..47e45a8de 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -13,3 +13,9 @@ rewrite.rules = [ AsciiSortImports PreferCurlyFors ] + +fileOverride { + "glob:**/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/**Routes*.scala" { + indentOperator.excludeRegex = "^.*~.*$" + } +} diff --git a/src/main/resources/swagger/api-docs.yaml b/src/main/resources/swagger/api-docs.yaml index 44887a06c..72d302c06 100755 --- a/src/main/resources/swagger/api-docs.yaml +++ b/src/main/resources/swagger/api-docs.yaml @@ -1789,7 +1789,7 @@ paths: patch: tags: - Resources - summary: Update the groups in the Auth Domain for a resource + summary: Add groups to the Auth Domain for a resource (removal of groups is not permitted) operationId: patchAuthDomainV2 parameters: - name: resourceTypeName @@ -3196,7 +3196,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TermsOfService' + $ref: '#/components/schemas/TermsOfServiceConfigResponse' /version: get: tags: @@ -3834,7 +3834,7 @@ components: type: boolean description: true if the user is in their proxy group description: "" - TermsOfService: + TermsOfServiceConfigResponse: required: - enforced - currentVersion diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala index fce64b8d6..415cc26c4 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala @@ -30,8 +30,8 @@ trait AdminRoutes extends SecurityDirectives with SamRequestContextDirectives wi pathPrefix("admin") { adminUserRoutes(user, requestContext) ~ pathPrefix("v1") { adminUserRoutes(user, requestContext) ~ - adminResourcesRoutes(user, requestContext) ~ - adminResourceTypesRoutes(user, requestContext) + adminResourcesRoutes(user, requestContext) ~ + adminResourceTypesRoutes(user, requestContext) } ~ pathPrefix("v2") { asWorkbenchAdmin(user) { adminUserRoutesV2(user, requestContext) @@ -49,76 +49,76 @@ trait AdminRoutes extends SecurityDirectives with SamRequestContextDirectives wi .map(status => (if (status.isDefined) OK else NotFound) -> status) } } ~ - pathPrefix(Segment) { userId => - pathEnd { - delete { + pathPrefix(Segment) { userId => + pathEnd { + delete { + complete { + userService.deleteUser(WorkbenchUserId(userId), samRequestContext).map(_ => OK) + } + } ~ + get { + complete { + userService + .getUserStatus(WorkbenchUserId(userId), samRequestContext = samRequestContext) + .map(status => (if (status.isDefined) OK else NotFound) -> status) + } + } ~ + patch { + entity(as[AdminUpdateUserRequest]) { request => complete { - userService.deleteUser(WorkbenchUserId(userId), samRequestContext).map(_ => OK) + userService + .updateUserCrud(WorkbenchUserId(userId), request, samRequestContext) + .map(user => (if (user.isDefined) OK else NotFound) -> user) } - } ~ - get { - complete { - userService - .getUserStatus(WorkbenchUserId(userId), samRequestContext = samRequestContext) - .map(status => (if (status.isDefined) OK else NotFound) -> status) - } - } ~ - patch { - entity(as[AdminUpdateUserRequest]) { request => - complete { - userService - .updateUserCrud(WorkbenchUserId(userId), request, samRequestContext) - .map(user => (if (user.isDefined) OK else NotFound) -> user) - } - } - } - } ~ - pathPrefix("enable") { - pathEndOrSingleSlash { - put { - complete { - userService - .enableUser(WorkbenchUserId(userId), samRequestContext) - .map(status => (if (status.isDefined) OK else NotFound) -> status) - } - } + } + } + } ~ + pathPrefix("enable") { + pathEndOrSingleSlash { + put { + complete { + userService + .enableUser(WorkbenchUserId(userId), samRequestContext) + .map(status => (if (status.isDefined) OK else NotFound) -> status) } - } ~ - pathPrefix("disable") { - pathEndOrSingleSlash { - put { - complete { - userService - .disableUser(WorkbenchUserId(userId), samRequestContext) - .map(status => (if (status.isDefined) OK else NotFound) -> status) - } - } + } + } + } ~ + pathPrefix("disable") { + pathEndOrSingleSlash { + put { + complete { + userService + .disableUser(WorkbenchUserId(userId), samRequestContext) + .map(status => (if (status.isDefined) OK else NotFound) -> status) } - } ~ - // This will get removed once ID-87 is resolved - pathPrefix("repairAllUsersGroup") { - pathEndOrSingleSlash { - put { - complete { - userService - .addToAllUsersGroup(WorkbenchUserId(userId), samRequestContext) - .map(_ => OK) - } - } + } + } + } ~ + // This will get removed once ID-87 is resolved + pathPrefix("repairAllUsersGroup") { + pathEndOrSingleSlash { + put { + complete { + userService + .addToAllUsersGroup(WorkbenchUserId(userId), samRequestContext) + .map(_ => OK) } - } ~ - pathPrefix("petServiceAccount") { - path(Segment) { project => - delete { - complete { - cloudExtensions - .deleteUserPetServiceAccount(WorkbenchUserId(userId), GoogleProject(project), samRequestContext) - .map(_ => NoContent) - } - } + } + } + } ~ + pathPrefix("petServiceAccount") { + path(Segment) { project => + delete { + complete { + cloudExtensions + .deleteUserPetServiceAccount(WorkbenchUserId(userId), GoogleProject(project), samRequestContext) + .map(_ => NoContent) } } + } } + } } } @@ -152,31 +152,31 @@ trait AdminRoutes extends SecurityDirectives with SamRequestContextDirectives wi } } } ~ - pathPrefix(Segment / "memberEmails" / Segment) { case (policyName, userEmail) => - val policyId = FullyQualifiedPolicyId(resource, AccessPolicyName(policyName)) - pathEndOrSingleSlash { - withSubject(WorkbenchEmail(userEmail), samRequestContext) { subject => - put { - requireAdminResourceAction(adminAddMember, resourceType, user, samRequestContext) { - complete { - resourceService - .addSubjectToPolicy(policyId, subject, samRequestContext) - .as(NoContent) - } + pathPrefix(Segment / "memberEmails" / Segment) { case (policyName, userEmail) => + val policyId = FullyQualifiedPolicyId(resource, AccessPolicyName(policyName)) + pathEndOrSingleSlash { + withSubject(WorkbenchEmail(userEmail), samRequestContext) { subject => + put { + requireAdminResourceAction(adminAddMember, resourceType, user, samRequestContext) { + complete { + resourceService + .addSubjectToPolicy(policyId, subject, samRequestContext) + .as(NoContent) } - } ~ - delete { - requireAdminResourceAction(adminRemoveMember, resourceType, user, samRequestContext) { - complete { - resourceService - .removeSubjectFromPolicy(policyId, subject, samRequestContext) - .as(NoContent) - } - } + } + } ~ + delete { + requireAdminResourceAction(adminRemoveMember, resourceType, user, samRequestContext) { + complete { + resourceService + .removeSubjectFromPolicy(policyId, subject, samRequestContext) + .as(NoContent) } + } } } } + } } } @@ -194,25 +194,25 @@ trait AdminRoutes extends SecurityDirectives with SamRequestContextDirectives wi } } } ~ - pathPrefix(Segment) { policyName => - val policyId = FullyQualifiedPolicyId(resource, AccessPolicyName(policyName)) - pathEndOrSingleSlash { - put { - entity(as[AccessPolicyMembershipRequest]) { membershipUpdate => - withResourceType(resourceTypeAdminName) { resourceTypeAdmin => - complete { - resourceService - .overwriteAdminPolicy(resourceTypeAdmin, policyId.accessPolicyName, policyId.resource, membershipUpdate, samRequestContext) - .as(Created) - } + pathPrefix(Segment) { policyName => + val policyId = FullyQualifiedPolicyId(resource, AccessPolicyName(policyName)) + pathEndOrSingleSlash { + put { + entity(as[AccessPolicyMembershipRequest]) { membershipUpdate => + withResourceType(resourceTypeAdminName) { resourceTypeAdmin => + complete { + resourceService + .overwriteAdminPolicy(resourceTypeAdmin, policyId.accessPolicyName, policyId.resource, membershipUpdate, samRequestContext) + .as(Created) } } - } ~ - delete { - complete(resourceService.deletePolicy(policyId, samRequestContext).as(NoContent)) - } + } + } ~ + delete { + complete(resourceService.deletePolicy(policyId, samRequestContext).as(NoContent)) } } + } } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala index 60e8492c1..ac5c2f934 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala @@ -43,67 +43,67 @@ trait OldUserRoutes extends SamUserDirectives with SamRequestContextDirectives { } } } ~ - (changeForbiddenToNotFound & withUserAllowInactive(samRequestContext)) { user => - get { - parameter("userDetailsOnly".?) { userDetailsOnly => - complete { - userService.getUserStatus(user.id, userDetailsOnly.exists(_.equalsIgnoreCase("true")), samRequestContext).map { statusOption => - statusOption - .map { status => - StatusCodes.OK -> Option(status) - } - .getOrElse(StatusCodes.NotFound -> None) - } + (changeForbiddenToNotFound & withUserAllowInactive(samRequestContext)) { user => + get { + parameter("userDetailsOnly".?) { userDetailsOnly => + complete { + userService.getUserStatus(user.id, userDetailsOnly.exists(_.equalsIgnoreCase("true")), samRequestContext).map { statusOption => + statusOption + .map { status => + StatusCodes.OK -> Option(status) + } + .getOrElse(StatusCodes.NotFound -> None) } } } } + } } ~ - pathPrefix("termsofservice") { - pathPrefix("status") { - pathEndOrSingleSlash { - get { - withUserAllowInactive(samRequestContext) { samUser => - complete { - tosService.getTosComplianceStatus(samUser, samRequestContext).map { tosAcceptanceStatus => - StatusCodes.OK -> Option(JsBoolean(tosAcceptanceStatus.permitsSystemUsage)) - } + pathPrefix("termsofservice") { + pathPrefix("status") { + pathEndOrSingleSlash { + get { + withUserAllowInactive(samRequestContext) { samUser => + complete { + tosService.getTosComplianceStatus(samUser, samRequestContext).map { tosAcceptanceStatus => + StatusCodes.OK -> Option(JsBoolean(tosAcceptanceStatus.permitsSystemUsage)) } } } } - } ~ - pathEndOrSingleSlash { - post { - withUserAllowInactive(samRequestContext) { samUser => - withTermsOfServiceAcceptance { - complete { - userService.acceptTermsOfService(samUser.id, samRequestContext).map { userStatusOption => - userStatusOption - .map { status => - StatusCodes.OK -> Option(status) - } - .getOrElse(StatusCodes.NotFound -> None) + } + } ~ + pathEndOrSingleSlash { + post { + withUserAllowInactive(samRequestContext) { samUser => + withTermsOfServiceAcceptance { + complete { + userService.acceptTermsOfService(samUser.id, samRequestContext).map { userStatusOption => + userStatusOption + .map { status => + StatusCodes.OK -> Option(status) } - } + .getOrElse(StatusCodes.NotFound -> None) } } - } ~ - delete { - withUserAllowInactive(samRequestContext) { samUser => - complete { - userService.rejectTermsOfService(samUser.id, samRequestContext).map { userStatusOption => - userStatusOption - .map { status => - StatusCodes.OK -> Option(status) - } - .getOrElse(StatusCodes.NotFound -> None) - } + } + } + } ~ + delete { + withUserAllowInactive(samRequestContext) { samUser => + complete { + userService.rejectTermsOfService(samUser.id, samRequestContext).map { userStatusOption => + userStatusOption + .map { status => + StatusCodes.OK -> Option(status) } - } + .getOrElse(StatusCodes.NotFound -> None) } + } } + } } + } } ~ pathPrefix("v2") { pathPrefix("self") { pathEndOrSingleSlash { @@ -115,38 +115,38 @@ trait OldUserRoutes extends SamUserDirectives with SamRequestContextDirectives { } } } ~ - (changeForbiddenToNotFound & withUserAllowInactive(samRequestContext)) { user => - path("info") { - get { - complete { - userService.getUserStatusInfo(user, samRequestContext) - } + (changeForbiddenToNotFound & withUserAllowInactive(samRequestContext)) { user => + path("info") { + get { + complete { + userService.getUserStatusInfo(user, samRequestContext) } - } ~ - path("diagnostics") { - get { - complete { - userService.getUserStatusDiagnostics(user.id, samRequestContext).map { statusOption => - statusOption - .map { status => - StatusCodes.OK -> Option(status) - } - .getOrElse(StatusCodes.NotFound -> None) + } + } ~ + path("diagnostics") { + get { + complete { + userService.getUserStatusDiagnostics(user.id, samRequestContext).map { statusOption => + statusOption + .map { status => + StatusCodes.OK -> Option(status) } - } - } - } ~ - path("termsOfServiceDetails") { - get { - complete(tosService.getTosDetails(user, samRequestContext)) - } - } ~ - path("termsOfServiceComplianceStatus") { - get { - complete(tosService.getTosComplianceStatus(user, samRequestContext)) + .getOrElse(StatusCodes.NotFound -> None) } } + } + } ~ + path("termsOfServiceDetails") { + get { + complete(tosService.getTosDetails(user, samRequestContext)) + } + } ~ + path("termsOfServiceComplianceStatus") { + get { + complete(tosService.getTosComplianceStatus(user, samRequestContext)) + } } + } } } } 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 d566b74b5..e66722048 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 @@ -41,13 +41,13 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM withNonAdminResourceType(ResourceTypeName(resourceTypeName)) { resourceType => pathEndOrSingleSlash { getUserPoliciesForResourceType(resourceType, samUser, samRequestContext) ~ - postResource(resourceType, samUser, samRequestContext) + postResource(resourceType, samUser, samRequestContext) } ~ pathPrefix(Segment) { resourceId => val resource = FullyQualifiedResourceId(resourceType.name, ResourceId(resourceId)) pathEndOrSingleSlash { deleteResource(resource, samUser, samRequestContext) ~ - postDefaultResource(resourceType, resource, samUser, samRequestContext) + postDefaultResource(resourceType, resource, samUser, samRequestContext) } ~ pathPrefix("action") { pathPrefix(Segment) { action => pathEndOrSingleSlash { @@ -72,7 +72,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM pathEndOrSingleSlash { getPolicy(policyId, samUser, samRequestContext) ~ - putPolicyOverwrite(resourceType, policyId, samUser, samRequestContext) + putPolicyOverwrite(resourceType, policyId, samUser, samRequestContext) } ~ pathPrefix("memberEmails") { pathEndOrSingleSlash { putPolicyMembershipOverwrite(policyId, samUser, samRequestContext) @@ -86,7 +86,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM samRequestContext ) { putUserInPolicy(policyId, subject, samRequestContext) ~ - deleteUserFromPolicy(policyId, subject, samRequestContext) + deleteUserFromPolicy(policyId, subject, samRequestContext) } } } @@ -94,7 +94,7 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM } ~ pathPrefix("public") { pathEndOrSingleSlash { getPublicFlag(policyId, samUser, samRequestContext) ~ - putPublicFlag(policyId, samUser, samRequestContext) + putPublicFlag(policyId, samUser, samRequestContext) } } } @@ -119,116 +119,116 @@ trait ResourceRoutes extends SamUserDirectives with SecurityDirectives with SamM withNonAdminResourceType(ResourceTypeName(resourceTypeName)) { resourceType => pathEndOrSingleSlash { getUserResourcesOfType(resourceType, samUser, samRequestContext) ~ - postResource(resourceType, samUser, samRequestContext) + postResource(resourceType, samUser, samRequestContext) } ~ - pathPrefix(Segment) { resourceId => - val resource = FullyQualifiedResourceId(resourceType.name, ResourceId(resourceId)) + pathPrefix(Segment) { resourceId => + val resource = FullyQualifiedResourceId(resourceType.name, ResourceId(resourceId)) + pathEndOrSingleSlash { + deleteResource(resource, samUser, samRequestContext) ~ + postDefaultResource(resourceType, resource, samUser, samRequestContext) + } ~ + pathPrefix("action") { + pathPrefix(Segment) { action => + pathEndOrSingleSlash { + getActionPermissionForUser(resource, samUser, action, samRequestContext) + } ~ + pathPrefix("userEmail") { + pathPrefix(Segment) { userEmail => + pathEndOrSingleSlash { + getActionPermissionForUserEmail(resource, samUser, ResourceAction(action), WorkbenchEmail(userEmail), samRequestContext) + } + } + } + } + } ~ + pathPrefix("leave") { + pathEndOrSingleSlash { + leaveResource(resourceType, resource, samUser, samRequestContext) + } + } ~ + pathPrefix("authDomain") { + pathEndOrSingleSlash { + getResourceAuthDomain(resource, samUser, samRequestContext) ~ + patchResourceAuthDomain(resource, samUser, samRequestContext) + } + } ~ + pathPrefix("roles") { + pathEndOrSingleSlash { + getUserResourceRoles(resource, samUser, samRequestContext) + } + } ~ + pathPrefix("actions") { + pathEndOrSingleSlash { + listActionsForUser(resource, samUser, samRequestContext) + } + } ~ + pathPrefix("allUsers") { + pathEndOrSingleSlash { + getAllResourceUsers(resource, samUser, samRequestContext) + } + } ~ + pathPrefix("parent") { + pathEndOrSingleSlash { + getResourceParent(resource, samUser, samRequestContext) ~ + setResourceParent(resource, samUser, samRequestContext) ~ + deleteResourceParent(resource, samUser, samRequestContext) + } + } ~ + pathPrefix("children") { pathEndOrSingleSlash { - deleteResource(resource, samUser, samRequestContext) ~ - postDefaultResource(resourceType, resource, samUser, samRequestContext) - } ~ - pathPrefix("action") { - pathPrefix(Segment) { action => + getResourceChildren(resource, samUser, samRequestContext) + } + } ~ + pathPrefix("policies") { + pathEndOrSingleSlash { + getResourcePolicies(resource, samUser, samRequestContext) + } ~ pathPrefix(Segment) { policyName => + val policyId = FullyQualifiedPolicyId(resource, AccessPolicyName(policyName)) + + pathEndOrSingleSlash { + getPolicy(policyId, samUser, samRequestContext) ~ + putPolicyOverwrite(resourceType, policyId, samUser, samRequestContext) ~ + deletePolicy(policyId, samUser, samRequestContext) + } ~ + pathPrefix("memberEmails") { + requireActionsForSharePolicy(policyId, samUser, samRequestContext) { pathEndOrSingleSlash { - getActionPermissionForUser(resource, samUser, action, samRequestContext) + putPolicyMembershipOverwrite(policyId, samUser, samRequestContext) } ~ - pathPrefix("userEmail") { - pathPrefix(Segment) { userEmail => - pathEndOrSingleSlash { - getActionPermissionForUserEmail(resource, samUser, ResourceAction(action), WorkbenchEmail(userEmail), samRequestContext) - } + pathPrefix(Segment) { email => + withSubject(WorkbenchEmail(email), samRequestContext) { subject => + pathEndOrSingleSlash { + putUserInPolicy(policyId, subject, samRequestContext) ~ + deleteUserFromPolicy(policyId, subject, samRequestContext) } } + } } } ~ - pathPrefix("leave") { - pathEndOrSingleSlash { - leaveResource(resourceType, resource, samUser, samRequestContext) - } - } ~ - pathPrefix("authDomain") { - pathEndOrSingleSlash { - getResourceAuthDomain(resource, samUser, samRequestContext) ~ - patchResourceAuthDomain(resource, samUser, samRequestContext) - } - } ~ - pathPrefix("roles") { - pathEndOrSingleSlash { - getUserResourceRoles(resource, samUser, samRequestContext) - } - } ~ - pathPrefix("actions") { - pathEndOrSingleSlash { - listActionsForUser(resource, samUser, samRequestContext) - } - } ~ - pathPrefix("allUsers") { - pathEndOrSingleSlash { - getAllResourceUsers(resource, samUser, samRequestContext) - } - } ~ - pathPrefix("parent") { - pathEndOrSingleSlash { - getResourceParent(resource, samUser, samRequestContext) ~ - setResourceParent(resource, samUser, samRequestContext) ~ - deleteResourceParent(resource, samUser, samRequestContext) - } - } ~ - pathPrefix("children") { - pathEndOrSingleSlash { - getResourceChildren(resource, samUser, samRequestContext) - } - } ~ - pathPrefix("policies") { - pathEndOrSingleSlash { - getResourcePolicies(resource, samUser, samRequestContext) - } ~ pathPrefix(Segment) { policyName => - val policyId = FullyQualifiedPolicyId(resource, AccessPolicyName(policyName)) - - pathEndOrSingleSlash { - getPolicy(policyId, samUser, samRequestContext) ~ - putPolicyOverwrite(resourceType, policyId, samUser, samRequestContext) ~ - deletePolicy(policyId, samUser, samRequestContext) - } ~ - pathPrefix("memberEmails") { - requireActionsForSharePolicy(policyId, samUser, samRequestContext) { - pathEndOrSingleSlash { - putPolicyMembershipOverwrite(policyId, samUser, samRequestContext) - } ~ - pathPrefix(Segment) { email => - withSubject(WorkbenchEmail(email), samRequestContext) { subject => - pathEndOrSingleSlash { - putUserInPolicy(policyId, subject, samRequestContext) ~ - deleteUserFromPolicy(policyId, subject, samRequestContext) - } - } - } - } - } ~ - pathPrefix("memberPolicies") { - requireActionsForSharePolicy(policyId, samUser, samRequestContext) { - path(Segment / Segment / Segment) { (memberResourceType, memberResourceId, memberPolicyName) => - val memberResource = FullyQualifiedResourceId(ResourceTypeName(memberResourceType), ResourceId(memberResourceId)) - val policySubject = FullyQualifiedPolicyId(memberResource, AccessPolicyName(memberPolicyName)) - withPolicy(policySubject, samRequestContext) { memberPolicy => - pathEndOrSingleSlash { - putUserInPolicy(policyId, memberPolicy.id, samRequestContext) ~ - deleteUserFromPolicy(policyId, memberPolicy.id, samRequestContext) - } - } - } - } - } ~ - pathPrefix("public") { + pathPrefix("memberPolicies") { + requireActionsForSharePolicy(policyId, samUser, samRequestContext) { + path(Segment / Segment / Segment) { (memberResourceType, memberResourceId, memberPolicyName) => + val memberResource = FullyQualifiedResourceId(ResourceTypeName(memberResourceType), ResourceId(memberResourceId)) + val policySubject = FullyQualifiedPolicyId(memberResource, AccessPolicyName(memberPolicyName)) + withPolicy(policySubject, samRequestContext) { memberPolicy => pathEndOrSingleSlash { - getPublicFlag(policyId, samUser, samRequestContext) ~ - putPublicFlag(policyId, samUser, samRequestContext) + putUserInPolicy(policyId, memberPolicy.id, samRequestContext) ~ + deleteUserFromPolicy(policyId, memberPolicy.id, samRequestContext) } } + } + } + } ~ + pathPrefix("public") { + pathEndOrSingleSlash { + getPublicFlag(policyId, samUser, samRequestContext) ~ + putPublicFlag(policyId, samUser, samRequestContext) } } + } } + } } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala index 7158bf917..a76f5ddc6 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala @@ -61,30 +61,30 @@ abstract class SamRoutes( def route: server.Route = (logRequestResult & handleExceptions(myExceptionHandler)) { oidcConfig.swaggerRoutes("swagger/api-docs.yaml") ~ - oidcConfig.oauth2Routes ~ - statusRoutes ~ - oldTermsOfServiceRoutes ~ - termsOfServiceRoutes ~ - withExecutionContext(ExecutionContext.global) { - withSamRequestContext { samRequestContext => - pathPrefix("register")(oldUserRoutes(samRequestContext)) ~ - pathPrefix("api") { - // these routes are for machine to machine authorized requests - // the whitelisted service admin account email is in the header of the request - serviceAdminRoutes(samRequestContext) ~ - userRoutesV2(samRequestContext) ~ - withActiveUser(samRequestContext) { samUser => - val samRequestContextWithUser = samRequestContext.copy(samUser = Option(samUser)) - resourceRoutes(samUser, samRequestContextWithUser) ~ - adminRoutes(samUser, samRequestContextWithUser) ~ - extensionRoutes(samUser, samRequestContextWithUser) ~ - groupRoutes(samUser, samRequestContextWithUser) ~ - azureRoutes(samUser, samRequestContextWithUser) ~ - userRoutesV1(samUser, samRequestContextWithUser) - } - } + oidcConfig.oauth2Routes ~ + statusRoutes ~ + oldTermsOfServiceRoutes ~ + termsOfServiceRoutes ~ + withExecutionContext(ExecutionContext.global) { + withSamRequestContext { samRequestContext => + pathPrefix("register")(oldUserRoutes(samRequestContext)) ~ + pathPrefix("api") { + // these routes are for machine to machine authorized requests + // the whitelisted service admin account email is in the header of the request + serviceAdminRoutes(samRequestContext) ~ + userRoutesV2(samRequestContext) ~ + withActiveUser(samRequestContext) { samUser => + val samRequestContextWithUser = samRequestContext.copy(samUser = Option(samUser)) + resourceRoutes(samUser, samRequestContextWithUser) ~ + adminRoutes(samUser, samRequestContextWithUser) ~ + extensionRoutes(samUser, samRequestContextWithUser) ~ + groupRoutes(samUser, samRequestContextWithUser) ~ + azureRoutes(samUser, samRequestContextWithUser) ~ + userRoutesV1(samUser, samRequestContextWithUser) + } } } + } } // basis for logRequestResult lifted from http://stackoverflow.com/questions/32475471/how-does-one-log-akka-http-client-requests diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala index 0b73e77b2..17078fdbe 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala @@ -4,7 +4,6 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server import akka.http.scaladsl.server.Directives._ import org.broadinstitute.dsde.workbench.sam.service.TosService -import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import scala.concurrent.ExecutionContext @@ -42,58 +41,58 @@ trait TermsOfServiceRoutes { complete(tosService.getTosConfig()) } } ~ - pathPrefix("docs") { // api/termsOfService/v1/docs + pathPrefix("docs") { // api/termsOfService/v1/docs + pathEndOrSingleSlash { + get { + complete(StatusCodes.NotImplemented) + } + } ~ + pathPrefix("redirect") { // api/termsOfService/v1/docs/redirect + pathEndOrSingleSlash { + get { + complete(StatusCodes.NotImplemented) + } + } + } + } ~ + pathPrefix("user") { // api/termsOfService/v1/user + pathPrefix("self") { // api/termsOfService/v1/user/self pathEndOrSingleSlash { get { complete(StatusCodes.NotImplemented) } } ~ - pathPrefix("redirect") { // api/termsOfService/v1/docs/redirect - pathEndOrSingleSlash { - get { - complete(StatusCodes.NotImplemented) - } + pathPrefix("accept") { // api/termsOfService/v1/user/accept + pathEndOrSingleSlash { + put { + complete(StatusCodes.NotImplemented) + } + } + } ~ + pathPrefix("reject") { // api/termsOfService/v1/user/reject + pathEndOrSingleSlash { + put { + complete(StatusCodes.NotImplemented) } } + } } ~ - pathPrefix("user") { // api/termsOfService/v1/user - pathPrefix("self") { // api/termsOfService/v1/user/self + // The {user_id} route must be last otherwise it will try to parse the other routes incorrectly as user id's + pathPrefix(Segment) { userId => // api/termsOfService/v1/user/{userId} + pathEndOrSingleSlash { + get { + complete(StatusCodes.NotImplemented) + } + } ~ + pathPrefix("history") { // api/termsOfService/v1/user/{userId}/history pathEndOrSingleSlash { get { complete(StatusCodes.NotImplemented) } - } ~ - pathPrefix("accept") { // api/termsOfService/v1/user/accept - pathEndOrSingleSlash { - put { - complete(StatusCodes.NotImplemented) - } - } - } ~ - pathPrefix("reject") { // api/termsOfService/v1/user/reject - pathEndOrSingleSlash { - put { - complete(StatusCodes.NotImplemented) - } - } - } - } ~ - // The {user_id} route must be last otherwise it will try to parse the other routes incorrectly as user id's - pathPrefix(Segment) { userId => // api/termsOfService/v1/user/{userId} - pathEndOrSingleSlash { - get { - complete(StatusCodes.NotImplemented) - } - } ~ - pathPrefix("history") { // api/termsOfService/v1/user/{userId}/history - pathEndOrSingleSlash { - get { - complete(StatusCodes.NotImplemented) - } - } - } } + } } + } } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala index fc6da229f..685019f8e 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala @@ -28,17 +28,17 @@ trait UserRoutesV1 extends SamUserDirectives with SamRequestContextDirectives { } } } ~ - pathPrefix("invite") { - post { - path(Segment) { inviteeEmail => - complete { - userService - .inviteUser(WorkbenchEmail(inviteeEmail.trim), samRequestContext) - .map(userStatus => StatusCodes.Created -> userStatus) - } + pathPrefix("invite") { + post { + path(Segment) { inviteeEmail => + complete { + userService + .inviteUser(WorkbenchEmail(inviteeEmail.trim), samRequestContext) + .map(userStatus => StatusCodes.Created -> userStatus) } } } + } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala index a8481d194..486490c4d 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala @@ -52,33 +52,33 @@ trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives { pathEndOrSingleSlash { getSamUserResponse(samUser, samRequestContext) } ~ - // api/users/v2/self/allowed - pathPrefix("allowed") { - pathEndOrSingleSlash { - getSamUserAllowances(samUser, samRequestContext) - } - } ~ - // api/user/v2/self/attributes - pathPrefix("attributes") { - pathEndOrSingleSlash { - getSamUserAttributes(samUser, samRequestContext) ~ - patchSamUserAttributes(samUser, samRequestContext) - } + // api/users/v2/self/allowed + pathPrefix("allowed") { + pathEndOrSingleSlash { + getSamUserAllowances(samUser, samRequestContext) + } + } ~ + // api/user/v2/self/attributes + pathPrefix("attributes") { + pathEndOrSingleSlash { + getSamUserAttributes(samUser, samRequestContext) ~ + patchSamUserAttributes(samUser, samRequestContext) } + } } ~ - pathPrefix(Segment) { samUserId => - val workbenchUserId = WorkbenchUserId(samUserId) - // api/users/v2/{sam_user_id} + pathPrefix(Segment) { samUserId => + val workbenchUserId = WorkbenchUserId(samUserId) + // api/users/v2/{sam_user_id} + pathEndOrSingleSlash { + regularUserOrAdmin(samUser, workbenchUserId, samRequestContext)(getSamUserResponse)(getAdminSamUserResponse) + } ~ + // api/users/v2/{sam_user_id}/allowed + pathPrefix("allowed") { pathEndOrSingleSlash { - regularUserOrAdmin(samUser, workbenchUserId, samRequestContext)(getSamUserResponse)(getAdminSamUserResponse) - } ~ - // api/users/v2/{sam_user_id}/allowed - pathPrefix("allowed") { - pathEndOrSingleSlash { - regularUserOrAdmin(samUser, workbenchUserId, samRequestContext)(getSamUserAllowances)(getAdminSamUserAllowances) - } - } + regularUserOrAdmin(samUser, workbenchUserId, samRequestContext)(getSamUserAllowances)(getAdminSamUserAllowances) + } } + } } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala index 57c3e2370..6d6a35bdb 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala @@ -78,7 +78,6 @@ object UserStatusDetails { @Deprecated @Lenses final case class TermsOfServiceDetails(isEnabled: Boolean, isGracePeriodEnabled: Boolean, currentVersion: String, userAcceptedVersion: Option[String]) -@Lenses final case class TermsOfServiceResponse(enforced: Boolean, currentVersion: String, inGracePeriod: Boolean, inRollingAcceptanceWindow: Boolean) @Lenses final case class ResourceActionPattern(value: String, description: String, authDomainConstrainable: Boolean) { def matches(other: ResourceAction) = value.r.pattern.matcher(other.value).matches() } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala index 3b44b0109..257b3383e 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala @@ -27,7 +27,6 @@ import org.broadinstitute.dsde.workbench.sam.model.{ TermsOfServiceAcceptance, TermsOfServiceComplianceStatus, TermsOfServiceDetails, - TermsOfServiceResponse, UserIdInfo, UserPolicyResponse, UserResourcesResponse, @@ -70,8 +69,6 @@ object SamJsonSupport { implicit val termsOfServiceDetailsFormat = jsonFormat4(TermsOfServiceDetails.apply) - implicit val termsOfServiceResponseFormat = jsonFormat4(TermsOfServiceResponse.apply) - implicit val termsOfAcceptanceStatusFormat = jsonFormat3(TermsOfServiceComplianceStatus.apply) implicit val UserStatusDiagnosticsFormat = jsonFormat5(UserStatusDiagnostics.apply) diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/TermsOfServiceConfigResponse.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/TermsOfServiceConfigResponse.scala new file mode 100644 index 000000000..867593587 --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/TermsOfServiceConfigResponse.scala @@ -0,0 +1,10 @@ +package org.broadinstitute.dsde.workbench.sam.model.api + +import monocle.macros.Lenses +import spray.json.DefaultJsonProtocol._ +import spray.json.RootJsonFormat +case object TermsOfServiceConfigResponse { + implicit val termsOfServiceResponseFormat: RootJsonFormat[TermsOfServiceConfigResponse] = jsonFormat4(TermsOfServiceConfigResponse.apply) +} + +@Lenses final case class TermsOfServiceConfigResponse(enforced: Boolean, currentVersion: String, inGracePeriod: Boolean, inRollingAcceptanceWindow: Boolean) diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala index fb68e580e..e0fc8bd90 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala @@ -16,7 +16,8 @@ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.sam.config.TermsOfServiceConfig import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.model.api.SamUser -import org.broadinstitute.dsde.workbench.sam.model.{SamUserTos, TermsOfServiceComplianceStatus, TermsOfServiceDetails, TermsOfServiceResponse} +import org.broadinstitute.dsde.workbench.sam.model.{SamUserTos, TermsOfServiceComplianceStatus, TermsOfServiceDetails} +import org.broadinstitute.dsde.workbench.sam.model.api.TermsOfServiceConfigResponse import java.io.{FileNotFoundException, IOException} import scala.concurrent.{Await, ExecutionContext} @@ -46,17 +47,18 @@ class TosService(val directoryDao: DirectoryDAO, val tosConfig: TermsOfServiceCo .rejectTermsOfService(userId, tosConfig.version, samRequestContext) .withInfoLogMessage(s"$userId has rejected version ${tosConfig.version} of the Terms of Service") - def getTosConfig(): IO[TermsOfServiceResponse] = { - val inRollingWindow = tosConfig.rollingAcceptanceWindowExpiration.exists(Instant.now().isAfter(_)) + def getTosConfig(): IO[TermsOfServiceConfigResponse] = IO.pure( - TermsOfServiceResponse( + TermsOfServiceConfigResponse( enforced = tosConfig.isTosEnabled, currentVersion = tosConfig.version, inGracePeriod = tosConfig.isGracePeriodEnabled, - inRollingAcceptanceWindow = inRollingWindow + inRollingAcceptanceWindow = isRollingWindowInEffect() ) ) - } + + private def isRollingWindowInEffect() = tosConfig.rollingAcceptanceWindowExpiration.exists(Instant.now().isBefore(_)) + @Deprecated def getTosDetails(samUser: SamUser, samRequestContext: SamRequestContext): IO[TermsOfServiceDetails] = directoryDao.getUserTos(samUser.id, samRequestContext).map { tos => @@ -89,12 +91,7 @@ class TosService(val directoryDao: DirectoryDAO, val tosConfig: TermsOfServiceCo val userCanUseSystemUnderGracePeriod = tosConfig.isGracePeriodEnabled && tos.action == TosTable.ACCEPT val userHasAcceptedPreviousVersion = userHasAcceptedPreviousTosVersion(previousUserTos) - val now = Instant.now() - val userInsideOfRollingAcceptanceWindow = tosConfig.rollingAcceptanceWindowExpiration match { - case Some(expiration) => - expiration.isAfter(now) && userHasAcceptedPreviousVersion - case None => false - } + val userInsideOfRollingAcceptanceWindow = isRollingWindowInEffect() && userHasAcceptedPreviousVersion userHasAcceptedLatestVersion || userInsideOfRollingAcceptanceWindow || userCanUseSystemUnderGracePeriod diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala index 95f47f72c..0fcba0f15 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala @@ -4,11 +4,10 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.sam.TestSupport import org.broadinstitute.dsde.workbench.sam.TestSupport.{databaseEnabled, databaseEnabledClue} -import org.broadinstitute.dsde.workbench.sam.model.TermsOfServiceResponse +import org.broadinstitute.dsde.workbench.sam.model.api.TermsOfServiceConfigResponse import org.scalatest.concurrent.Eventually.eventually import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ class TermsOfServiceRouteSpec extends AnyFlatSpec with Matchers with ScalatestRouteTest with TestSupport { @@ -19,7 +18,7 @@ class TermsOfServiceRouteSpec extends AnyFlatSpec with Matchers with ScalatestRo eventually { Get("/termsOfService/v1") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - responseAs[TermsOfServiceResponse] shouldBe TermsOfServiceResponse( + responseAs[TermsOfServiceConfigResponse] shouldBe TermsOfServiceConfigResponse( enforced = true, currentVersion = "0", inGracePeriod = false, diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala index 26c19b5b8..c584287e4 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala @@ -6,9 +6,11 @@ import cats.effect.IO import cats.effect.unsafe.implicits.{global => globalEc} import org.broadinstitute.dsde.workbench.model.WorkbenchUserId import org.broadinstitute.dsde.workbench.sam.TestSupport.tosConfig +import org.broadinstitute.dsde.workbench.sam.config.TermsOfServiceConfig import org.broadinstitute.dsde.workbench.sam.dataAccess.DirectoryDAO import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.model.SamUserTos +import org.broadinstitute.dsde.workbench.sam.model.api.TermsOfServiceConfigResponse import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.sam.{Generator, PropertyBasedTesting, TestSupport} import org.mockito.Mockito.RETURNS_SMART_NULLS @@ -58,6 +60,27 @@ class TosServiceSpec(_system: ActorSystem) TestSupport.tosConfig.isTosEnabled shouldBe true } + "returns configurations" in { + val tosConfig = TermsOfServiceConfig( + isTosEnabled = true, + isGracePeriodEnabled = true, + version = "1", + baseUrl = "https://sam.tos", + rollingAcceptanceWindowExpiration = Option(Instant.now().plusSeconds(3600)), + previousVersion = Option("0") + ) + val tosService = new TosService(dirDAO, tosConfig) + + // accept and get ToS status + val tosConfigResponse = tosService.getTosConfig().unsafeRunSync() + tosConfigResponse shouldBe TermsOfServiceConfigResponse( + enforced = true, + currentVersion = "1", + inGracePeriod = true, + inRollingAcceptanceWindow = true + ) + } + "accepts the ToS for a user" in { when(dirDAO.acceptTermsOfService(any[WorkbenchUserId], any[String], any[SamRequestContext])) .thenReturn(IO.pure(true))