support-frontend/app/actions/CustomActionBuilders.scala (107 lines of code) (raw):
package actions
import actions.AsyncAuthenticatedBuilder.OptionalAuthRequest
import admin.settings.{FeatureSwitches, On}
import com.gu.aws.{AwsCloudWatchMetricPut, AwsCloudWatchMetricSetup}
import com.gu.monitoring.SafeLogging
import com.gu.support.config.Stage
import models.identity.responses.IdentityErrorResponse._
import org.apache.pekko.stream.scaladsl.Flow
import org.apache.pekko.util.ByteString
import play.api.libs.streams.Accumulator
import play.api.mvc._
import play.filters.csrf._
import services.AsyncAuthenticationService
import services.RecaptchaResponse.recaptchaFailedCode
import utils.FastlyGEOIP
import scala.concurrent.{ExecutionContext, Future}
class CustomActionBuilders(
val asyncAuthenticationService: AsyncAuthenticationService,
userFromAuthCookiesOrAuthServerActionBuilder: UserFromAuthCookiesOrAuthServerActionBuilder,
userFromAuthCookiesActionBuilder: UserFromAuthCookiesActionBuilder,
cc: ControllerComponents,
addToken: CSRFAddToken,
checkToken: CSRFCheck,
csrfConfig: CSRFConfig,
stage: Stage,
featureSwitches: => FeatureSwitches,
)(implicit private val ec: ExecutionContext) {
val PrivateAction =
new PrivateActionBuilder(addToken, checkToken, csrfConfig, cc.parsers.defaultBodyParser, cc.executionContext)
/** This is the action builder that should be used for all actions requiring authentication that are triggered by a
* page load.
*
* The difference between this builder and [[MaybeAuthenticatedActionOnFormSubmission]] is that this builder will
* redirect to the auth server to find auth tokens if there are no cookies present containing them.
*/
def MaybeAuthenticatedAction: ActionBuilder[OptionalAuthRequest, AnyContent] =
chooseActionBuilder(userFromAuthCookiesOrAuthServerActionBuilder)
/** This is the action builder that should be used for all actions requiring authentication that are triggered by a
* form submission.
*
* The difference between this builder and [[MaybeAuthenticatedAction]] is that this builder will not redirect to the
* auth server. It just checks for the presence of cookies containing auth tokens.
*/
def MaybeAuthenticatedActionOnFormSubmission: ActionBuilder[OptionalAuthRequest, AnyContent] =
chooseActionBuilder(userFromAuthCookiesActionBuilder)
private def chooseActionBuilder(
oktaAuthBuilder: ActionBuilder[OptionalAuthRequest, AnyContent],
): ActionBuilder[OptionalAuthRequest, AnyContent] =
PrivateAction andThen (
if (featureSwitches.authenticateWithOkta.contains(On))
oktaAuthBuilder
else
new AsyncAuthenticatedBuilder(asyncAuthenticationService.tryAuthenticateUser, cc.parsers.defaultBodyParser)
)
case class LoggingAndAlarmOnFailure[A](chainedAction: Action[A]) extends EssentialAction with SafeLogging {
private def pushMetric(cloudwatchEvent: AwsCloudWatchMetricPut.MetricRequest) = {
AwsCloudWatchMetricPut(AwsCloudWatchMetricPut.client)(cloudwatchEvent)
}
private def pushAlarmMetric = {
val cloudwatchEvent = AwsCloudWatchMetricSetup.serverSideCreateFailure(stage)
pushMetric(cloudwatchEvent)
}
private def pushHighThresholdAlarmMetric = {
val cloudwatchEvent = AwsCloudWatchMetricSetup.serverSideHighThresholdCreateFailure(stage)
pushMetric(cloudwatchEvent)
}
private def maybePushAlarmMetric(result: Result) = {
// We'll never alarm on these
val ignoreList = Set(
emailProviderRejectedCode,
invalidEmailAddressCode,
recaptchaFailedCode,
)
// We'll alarm on these, but only over a certain threshold
val highThresholdList = Set(
emailAddressAlreadyTakenCode,
)
if (result.header.status == 500) {
if (ignoreList.contains(result.header.reasonPhrase.getOrElse(""))) {
logger.info(
s"not pushing alarm metric for ${result.header.status} ${result.header.reasonPhrase} as it is in our ignore list",
)
} else if (highThresholdList.contains(result.header.reasonPhrase.getOrElse(""))) {
logger.info(
s"pushing higher threshold alarm metric for ${result.header.status} ${result.header.reasonPhrase}",
)
pushHighThresholdAlarmMetric
} else {
logger.error(
scrub"pushing alarm metric - non 2xx response. Http code: ${result.header.status}, reason: ${result.header.reasonPhrase}",
)
pushAlarmMetric
}
}
}
def apply(requestHeader: RequestHeader): Accumulator[ByteString, Result] = {
val accumulator = chainedAction.apply(requestHeader)
val loggedAccumulator = accumulator.through(Flow.fromFunction { (byteString: ByteString) =>
logger.info("incoming POST: " + byteString.utf8String)
byteString
})
loggedAccumulator
.map { result =>
maybePushAlarmMetric(result)
result
}
.recoverWith({ case throwable: Throwable =>
logger.error(scrub"pushing alarm metric - 5xx response caused by ${throwable}")
pushAlarmMetric
Future.failed(throwable)
})
}
}
val CachedAction = new CachedAction(cc.parsers.defaultBodyParser, cc.executionContext)
val NoCacheAction = new NoCacheAction(cc.parsers.defaultBodyParser, cc.executionContext)
val GeoTargetedCachedAction = new CachedAction(
cc.parsers.defaultBodyParser,
cc.executionContext,
List("Vary" -> FastlyGEOIP.fastlyCountryHeader),
)
}