We use play-pac4j for our authentication in our play application.
We would like to have the same route/controller endpoint but with a different behaviour dependending on the user Role.
Conceptually, this would do something like:
val ACTION_ONE: ActionBuilder[Request, AnyContent] = Secure(
JWT_CLIENT, Authorizers.Role1
)(anyContentLarge)
val ACTION_TWO: ActionBuilder[Request, AnyContent] = Secure(
JWT_CLIENT, Authorizers.Role2
)(anyContentLarge)
def index = ACTION_ONE.async{ req => index1(req) } orElse ACTION_TWO.async{ req => index2(req) }
def index1(req: Request[AnyContent]) = //... behavior with role1
def index2(req: Request[AnyContent]) = //... behavior with role2
But the composition of Play Actions
provides only andThen
, no orElse
.
Is there a way to achieve that ?
CodePudding user response:
I don't think you'll be able to compose in a orElse
manner the Action
s.
However you should be able to create a "combined" ActionBuilder
that uses your 2 existing ActionBuilder
s and do the orElse
logic. Though you would only be able to provide one body to run. And this body would have to rely on something like the AuthenticatedRequest#profiles
to determine what to do.
Something like:
def index = ACTION_COMBINED.async{ req: AuthenticatedRequest =>
// Check something on req.profiles
if (...) index1(req) else index2(req)
}
I'm not familiar with play-pac4j to be more precise
CodePudding user response:
So finally I implemented it :)
It uses 'fallBackToNext', a method we already have in our codebase that behave as fallBackTo but with an async lambda parameter, so the next future is executed only if the first one is already a failure (preventing big computations from happening when not needed, but reducing parallelism).
Here are most of the logic:
/**
* This combination of action is the implementation class of the "orElse" operator,
* allowing to have one and only one action to be executed within the given actions
*/
class EitherActions[A](actions: Seq[Action[A]]) extends Action[A] {
require(actions.nonEmpty, "The actions to combine should not be empty")
override def parser: BodyParser[A] = actions.head.parser
override def executionContext: ExecutionContext = actions.head.executionContext
/**
* @param request
* @return either the first result to be successful, or the first to be failure
*/
override def apply(
request: Request[A]
): Future[Result] = {
// as we know actions is nonEmpty, we can start with actions.head and directly fold on actions.tail
// this removes the need to manage an awkward "zero" value in the fold
val firstResult = actions.head.apply(request)
// we wrap all apply() calls into changeUnauthorizedIntoFailure to be able to use fallbackToNext on 403
val finalResult = actions.tail.foldLeft( changeUnauthorizedIntoFailure(firstResult) ) {
( previousResult, nextAction ) =>
RichFuture(previousResult).fallbackToNext{ () =>
changeUnauthorizedIntoFailure(nextAction.apply(request))
}(executionContext)
}
// restore the original message
changeUnauthorizedIntoSuccess(finalResult)
}
/**
* to use fallBackToNext, we need to have failed Future, thus we change the Success(403) into a Failure(403)
* we keep the original result to be able to restore it at the end if none of the combined actions did success
*/
private def changeUnauthorizedIntoFailure(
before: Future[Result]
): Future[Result] = {
val after = before.transform {
case Success(originalResult) if originalResult.header.status == Unauthorized =>
Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
case Success(originalResult) if originalResult.header.status == Forbidden =>
Failure(EitherActions.UnauthorizedWrappedException(originalResult = originalResult))
case keepResult@_ => keepResult
}(executionContext)
after
}
/**
* after the last call, if we still have a UnauthorizedWrappedException, we change it back to a Success(403)
* to restore the original message
*/
private def changeUnauthorizedIntoSuccess(
before: Future[Result]
): Future[Result] = {
val after = before.transform {
case Failure(EitherActions.UnauthorizedWrappedException(_, _, result)) => Success(result)
case keepResult@_ => keepResult
}(executionContext)
after
}
def orElse( other: Action[A]): EitherActions[A] = {
new EitherActions[A]( actions : other)
}
}
object EitherActions {
private case class UnauthorizedWrappedException(
private val message: String = "",
private val cause: Throwable = None.orNull,
val originalResult: Result,
) extends Exception(message, cause)
}