Skip to content

Commit

Permalink
refacto: add a nice DSL for actions and use it everywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
eletallbetagouv committed Feb 11, 2025
1 parent 24da626 commit 3207a5c
Show file tree
Hide file tree
Showing 34 changed files with 358 additions and 315 deletions.
36 changes: 25 additions & 11 deletions app/authentication/actions/ImpersonationAction.scala
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
package authentication.actions

import authentication.actions.MaybeUserAction.MaybeUserRequest
import models.User
import play.api.mvc.Results.Forbidden
import play.api.mvc.ActionFilter
import play.api.mvc.Result
import utils.EmailAddress

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

object ImpersonationAction {
type UserRequest[A] = IdentifiedRequest[User, A]

def ForbidImpersonation(implicit ec: ExecutionContext): ActionFilter[UserRequest] = new ActionFilter[UserRequest] {
override protected def executionContext: ExecutionContext = ec

override protected def filter[A](request: UserRequest[A]): Future[Option[Result]] =
Future.successful(
request.identity.impersonator match {
case Some(_) => Some(Forbidden)
case None => None
}
)
}
def forbidImpersonationFilter(implicit ec: ExecutionContext): ActionFilter[UserRequest] =
new ActionFilter[UserRequest] {
override protected def executionContext: ExecutionContext = ec
override protected def filter[A](request: UserRequest[A]): Future[Option[Result]] =
handleImpersonator(request.identity.impersonator)

}

def forbidImpersonationOnMaybeUserFilter(implicit ec: ExecutionContext): ActionFilter[MaybeUserRequest] =
new ActionFilter[MaybeUserRequest] {
override protected def executionContext: ExecutionContext = ec
override protected def filter[A](request: MaybeUserRequest[A]): Future[Option[Result]] =
handleImpersonator(request.identity.flatMap(_.impersonator))
}

private def handleImpersonator(maybeImpersonator: Option[EmailAddress]) =
Future.successful(
maybeImpersonator match {
case Some(_) => Some(Forbidden)
case None => None
}
)

}
52 changes: 24 additions & 28 deletions app/controllers/AccountController.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package controllers

import authentication.CookieAuthenticator
import authentication.actions.ImpersonationAction.ForbidImpersonation
import authentication.actions.ImpersonationAction.forbidImpersonationFilter
import cats.implicits.catsSyntaxOption
import config.EmailConfiguration
import models._
Expand All @@ -13,8 +13,6 @@ import play.api.mvc.ControllerComponents
import repositories.user.UserRepositoryInterface
import utils.EmailAddress
import error.AppError.MalformedFileKey
import authentication.actions.UserAction.WithAuthProvider
import authentication.actions.UserAction.WithRole

import java.util.UUID
import scala.concurrent.ExecutionContext
Expand All @@ -37,7 +35,7 @@ class AccountController(

implicit val contactAddress: EmailAddress = emailConfiguration.contactAddress

def activateAccount = IpRateLimitedAction2.async(parse.json) { implicit request =>
def activateAccount = Act.public.standardLimit.async(parse.json) { implicit request =>
for {
activationRequest <- request.parseBody[ActivationRequest]()
createdUser <- activationRequest.companySiret match {
Expand All @@ -52,7 +50,7 @@ class AccountController(
}

def sendAgentInvitation(role: UserRole) =
SecuredAction.andThen(WithRole(UserRole.Admins)).async(parse.json) { implicit request =>
Act.secured.admins.async(parse.json) { implicit request =>
role match {
case UserRole.DGCCRF =>
request
Expand All @@ -69,7 +67,7 @@ class AccountController(
}

def sendAgentsInvitations(role: UserRole) =
SecuredAction.andThen(WithRole(UserRole.Admins)).async(parse.multipartFormData) { implicit request =>
Act.secured.admins.async(parse.multipartFormData) { implicit request =>
for {
filePart <- request.body.file("emails").liftTo[Future](MalformedFileKey("emails"))
source = Source.fromFile(filePart.ref.path.toFile)
Expand All @@ -80,7 +78,7 @@ class AccountController(
}

def sendAdminInvitation(role: UserRole) =
SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async(parse.json) { implicit request =>
Act.secured.superAdmins.async(parse.json) { implicit request =>
role match {
case UserRole.SuperAdmin =>
request
Expand All @@ -99,7 +97,7 @@ class AccountController(
}

def fetchPendingAgent(role: Option[UserRole]) =
SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnly)).async { _ =>
Act.secured.adminsAndReadonly.async { _ =>
role match {
case Some(UserRole.DGCCRF) | Some(UserRole.DGAL) | None =>
accessesOrchestrator
Expand All @@ -110,34 +108,34 @@ class AccountController(
}

def fetchAgentUsers =
SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnly)).async { _ =>
Act.secured.adminsAndReadonly.async { _ =>
for {
users <- userRepository.listForRoles(Seq(UserRole.DGCCRF, UserRole.DGAL))
} yield Ok(Json.toJson(users))
}

def fetchAdminUsers =
SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
Act.secured.superAdmins.async { _ =>
for {
users <- userRepository.listForRoles(Seq(UserRole.SuperAdmin, UserRole.Admin, UserRole.ReadOnlyAdmin))
} yield Ok(Json.toJson(users))
}

// This data is not displayed anywhere
// The endpoint might be useful to debug without accessing the prod DB
def fetchAllSoftDeletedUsers = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
def fetchAllSoftDeletedUsers = Act.secured.superAdmins.async { _ =>
for {
users <- userRepository.listDeleted()
} yield Ok(Json.toJson(users))
}

def fetchTokenInfo(token: String) = IpRateLimitedAction2.async { _ =>
def fetchTokenInfo(token: String) = Act.public.standardLimit.async { _ =>
accessesOrchestrator
.fetchDGCCRFUserActivationToken(token)
.map(token => Ok(Json.toJson(token)))
}

def validateEmail() = IpRateLimitedAction2.async(parse.json) { implicit request =>
def validateEmail() = Act.public.standardLimit.async(parse.json) { implicit request =>
for {
token <- request.parseBody[String](JsPath \ "token")
user <- accessesOrchestrator.validateAgentEmail(token)
Expand All @@ -146,34 +144,32 @@ class AccountController(
}

def forceValidateEmail(email: String) =
SecuredAction.andThen(WithRole(UserRole.Admins)).async { _ =>
Act.secured.admins.async { _ =>
accessesOrchestrator.resetLastEmailValidation(EmailAddress(email)).map(_ => NoContent)
}

def edit() =
SecuredAction.andThen(WithAuthProvider(AuthProvider.SignalConso)).andThen(ForbidImpersonation).async(parse.json) {
implicit request =>
for {
userUpdate <- request.parseBody[UserUpdate]()
updatedUserOpt <- userOrchestrator.edit(request.identity.id, userUpdate)
} yield updatedUserOpt match {
case Some(updatedUser) => Ok(Json.toJson(updatedUser))
case _ => NotFound
}
Act.secured.restrictByProvider.signalConso.andThen(forbidImpersonationFilter).async(parse.json) { implicit request =>
for {
userUpdate <- request.parseBody[UserUpdate]()
updatedUserOpt <- userOrchestrator.edit(request.identity.id, userUpdate)
} yield updatedUserOpt match {
case Some(updatedUser) => Ok(Json.toJson(updatedUser))
case _ => NotFound
}
}

def sendEmailAddressUpdateValidation() =
SecuredAction.andThen(ForbidImpersonation).async(parse.json) { implicit request =>
Act.secured.all.forbidImpersonation.async(parse.json) { implicit request =>
for {
emailAddress <- request.parseBody[EmailAddress](JsPath \ "email")
_ <- accessesOrchestrator.sendEmailAddressUpdateValidation(request.identity, emailAddress)
} yield NoContent
}

def updateEmailAddress(token: String) =
SecuredAction
.andThen(WithAuthProvider(AuthProvider.SignalConso))
.andThen(ForbidImpersonation)
Act.secured.restrictByProvider.signalConso
.andThen(forbidImpersonationFilter)
.async { implicit request =>
for {
updatedUser <- accessesOrchestrator.updateEmailAddress(request.identity, token)
Expand All @@ -182,7 +178,7 @@ class AccountController(
}

def softDelete(id: UUID) =
SecuredAction.andThen(WithRole(UserRole.Admins)).async { request =>
Act.secured.admins.async { request =>
userOrchestrator.softDelete(targetUserId = id, currentUserId = request.identity.id).map(_ => NoContent)
}

Expand Down
23 changes: 11 additions & 12 deletions app/controllers/AdminController.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package controllers

import authentication.Authenticator
import authentication.actions.UserAction.WithRole
import cats.data.NonEmptyList
import cats.implicits.catsSyntaxOption
import cats.implicits.toTraverseOps
Expand Down Expand Up @@ -131,11 +130,11 @@ class AdminController(
private val allEmailExamples: Seq[(String, (EmailAddress, MessagesApi) => BaseEmail)] =
allEmailDefinitions.flatMap(readExamplesWithFullKey)

def getEmailCodes = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
def getEmailCodes = Act.secured.superAdmins.async { _ =>
val keys = allEmailExamples.map(_._1)
Future.successful(Ok(Json.toJson(keys)))
}
def sendTestEmail(templateRef: String, to: String) = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
def sendTestEmail(templateRef: String, to: String) = Act.secured.superAdmins.async { _ =>
val maybeEmail = allEmailExamples
.find(_._1 == templateRef)
.map(_._2(EmailAddress(to), controllerComponents.messagesApi))
Expand All @@ -154,10 +153,10 @@ class AdminController(
s"${emailDefinition.category.toString.toLowerCase}.$key" -> fn
}

def getPdfCodes = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)) { _ =>
def getPdfCodes = Act.secured.superAdmins { _ =>
Ok(Json.toJson(availablePdfs.map(_._1)))
}
def sendTestPdf(templateRef: String) = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)) { _ =>
def sendTestPdf(templateRef: String) = Act.secured.superAdmins { _ =>
availablePdfs.toMap
.get(templateRef)
.map { html =>
Expand Down Expand Up @@ -265,7 +264,7 @@ class AdminController(
}

def resend(start: OffsetDateTime, end: OffsetDateTime, emailType: ResendEmailType) =
SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { implicit request =>
Act.secured.superAdmins.async { implicit request =>
for {
reports <- reportRepository.getReportsWithFiles(
Some(request.identity),
Expand All @@ -284,24 +283,24 @@ class AdminController(
} yield NoContent
}

def blackListedIPs() = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
def blackListedIPs() = Act.secured.superAdmins.async { _ =>
ipBlackListRepository.list().map(blackListedIps => Ok(Json.toJson(blackListedIps)))
}

def deleteBlacklistedIp(ip: String) = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
def deleteBlacklistedIp(ip: String) = Act.secured.superAdmins.async { _ =>
ipBlackListRepository.delete(ip).map(_ => NoContent)
}

def createBlacklistedIp() =
SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async(parse.json) { implicit request =>
Act.secured.superAdmins.async(parse.json) { implicit request =>
for {
blacklistedIpRequest <- request.parseBody[BlackListedIp]()
blackListedIp <- ipBlackListRepository.create(blacklistedIpRequest)
} yield Created(Json.toJson(blackListedIp))
}

def classifyAndSummarize(reportId: UUID) =
SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async { _ =>
Act.secured.adminsAndReadonlyAndAgents.allowImpersonation.async { _ =>
for {
maybeReport <- reportRepository.get(reportId)
report <- maybeReport.liftTo[Future](AppError.ReportNotFound(reportId))
Expand All @@ -323,13 +322,13 @@ class AdminController(
}

def getAlbertClassification(reportId: UUID) =
SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async { _ =>
Act.secured.adminsAndReadonlyAndAgents.allowImpersonation.async { _ =>
albertClassificationRepository
.getByReportId(reportId)
.map(maybeClassification => Ok(Json.toJson(maybeClassification)))
}

def regenSampleData() = SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { _ =>
def regenSampleData() = Act.secured.superAdmins.async { _ =>
if (taskConfiguration.sampleData.active) {
for {
_ <-
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/AsyncFileController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AsyncFileController(
)(implicit val ec: ExecutionContext)
extends BaseController(authenticator, controllerComponents) {

def listAsyncFiles(kind: Option[String]) = SecuredAction.async { implicit request =>
def listAsyncFiles(kind: Option[String]) = Act.secured.all.allowImpersonation.async { implicit request =>
for {
asyncFiles <- asyncFileRepository.list(request.identity, kind.map(AsyncFileKind.withName))
} yield Ok(Json.toJson(asyncFiles.map { asyncFile: AsyncFile =>
Expand Down
Loading

0 comments on commit 3207a5c

Please sign in to comment.