From d2fc3483f7ee5bbc9a25d00276b16e7557ca3741 Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 3 Apr 2024 13:14:36 +0100 Subject: [PATCH 1/3] add `dev-nginx` config for `admin` and `preview` so they can be hit on https://frontend.local.dev-gutools.co.uk and https://preview.local.dev-gutools.co.uk respectively. --- nginx/frontend.conf | 86 ++++++++++++++++++++++++++--------------- nginx/nginx-mapping.yml | 7 ++++ nginx/setup.sh | 2 + setup.sh | 10 +++++ 4 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 nginx/nginx-mapping.yml diff --git a/nginx/frontend.conf b/nginx/frontend.conf index fb8c6eb98656..435ecfbab13c 100644 --- a/nginx/frontend.conf +++ b/nginx/frontend.conf @@ -1,44 +1,66 @@ -upstream frontend { - server localhost:3000; - server localhost:9000 backup; +server { + listen 443 ssl; + server_name preview.local.dev-gutools.co.uk; + + location / { + proxy_http_version 1.1; + proxy_pass http://localhost:9000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect default; + proxy_buffering off; + } + + ssl_certificate preview.local.dev-gutools.co.uk.crt; + ssl_certificate_key preview.local.dev-gutools.co.uk.key; + + ssl_session_timeout 5m; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; } server { - proxy_http_version 1.1; # this is essential for chunked responses to work - server_name m.thegulocal.com; + listen 80; + server_name preview.local.dev-gutools.co.uk; - # Enforce HTTPS - return https://m.thegulocal.com$request_uri; + # redirect all HTTP traffic to HTTPS + return 301 https://$host$request_uri; } server { - proxy_http_version 1.1; # this is essential for chunked responses to work - listen 443 ssl; - server_name m.thegulocal.com; - - ssl_certificate m.thegulocal.com.crt; - ssl_certificate_key m.thegulocal.com.key; - ssl_session_timeout 5m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - - # dummy location header for the API - proxy_set_header X-GU-ID-Geolocation ip:$remote_addr,country:GB,city:Leeds; + listen 443 ssl; + server_name frontend.local.dev-gutools.co.uk; + + location / { + proxy_http_version 1.1; + proxy_pass http://localhost:9000; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect default; + proxy_buffering off; + } + + ssl_certificate frontend.local.dev-gutools.co.uk.crt; + ssl_certificate_key frontend.local.dev-gutools.co.uk.key; + + ssl_session_timeout 5m; - # some urls coming through as /assets/assets/path/to/file.js for some reason - rewrite ^/assets/assets/(.*)$ /assets/$1 permanent; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; +} - location ~* /assets/[A-Za-z\d\-_\.]*\.(js|css) { - proxy_pass http://127.0.0.1:3030; - proxy_set_header Host "http://localhost:3030"; - proxy_set_header "X-Forwarded-Proto" "https"; - } +server { + listen 80; + server_name frontend.local.dev-gutools.co.uk; - location / { - proxy_pass http://frontend; - proxy_set_header Host $host; - proxy_set_header "X-Forwarded-Proto" "https"; - } + # redirect all HTTP traffic to HTTPS + return 301 https://$host$request_uri; } + diff --git a/nginx/nginx-mapping.yml b/nginx/nginx-mapping.yml new file mode 100644 index 000000000000..65889a7ebdd3 --- /dev/null +++ b/nginx/nginx-mapping.yml @@ -0,0 +1,7 @@ +name: frontend +domain-root: local.dev-gutools.co.uk +mappings: + - port: 9000 + prefix: preview + - port: 9000 + prefix: frontend # for admin portal diff --git a/nginx/setup.sh b/nginx/setup.sh index 03d371682a7c..41fa9de1a16a 100755 --- a/nginx/setup.sh +++ b/nginx/setup.sh @@ -11,8 +11,10 @@ SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" dev-nginx setup-cert ${DOMAIN} dev-nginx link-config ${SOURCE_DIR}/frontend.conf +dev-nginx setup-app ./nginx-mapping.yml # for preview and admin dev-nginx restart-nginx echo -e "💯 Done! You can now run frontend locally on https://${DOMAIN}" +echo -e "You can also (separately) run admin and preview locally on https://frontend.local.dev-gutools.co.uk and https://preview.local.dev-gutools.co.uk respectively." echo -e "👤 To setup Dotcom Identity Frontend please follow identity-platform README." echo -e "👋" diff --git a/setup.sh b/setup.sh index 88758c87bf6e..a3fbdaa509b8 100755 --- a/setup.sh +++ b/setup.sh @@ -99,6 +99,16 @@ install_node() { fi } +install_dev-nginx() { + if ! installed nginx; then + if linux; then + sudo apt-get install -y nginx + elif mac; then + brew install nginx + fi + fi +} + install_gcc() { if ! installed g++; then if linux; then From 185005683139e273d87dd59e4e984bc85dae309f Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Wed, 3 Apr 2024 13:27:40 +0100 Subject: [PATCH 2/3] =?UTF-8?q?rename=20`GoogleAuthFilters`=20to=20`Common?= =?UTF-8?q?AuthFilters`=20in=20anticipation=20of=20replacing=20plain=20Goo?= =?UTF-8?q?gle=20auth=20with=20`pan-domain-authentication`=20=F0=9F=90=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/app/http/AdminFilters.scala | 3 +-- .../http/{GoogleAuthFilters.scala => CommonAuthFilters.scala} | 2 +- preview/app/http/PreviewFilters.scala | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) rename common/app/http/{GoogleAuthFilters.scala => CommonAuthFilters.scala} (98%) diff --git a/admin/app/http/AdminFilters.scala b/admin/app/http/AdminFilters.scala index cd73190ba8a6..b9cf11b4a333 100644 --- a/admin/app/http/AdminFilters.scala +++ b/admin/app/http/AdminFilters.scala @@ -1,8 +1,7 @@ package http import org.apache.pekko.stream.Materializer -import GoogleAuthFilters.AuthFilterWithExemptions -import googleAuth.FilterExemptions +import CommonAuthFilters.AuthFilterWithExemptions import model.ApplicationContext import play.api.http.{HttpConfiguration, HttpFilters} import play.api.mvc.EssentialFilter diff --git a/common/app/http/GoogleAuthFilters.scala b/common/app/http/CommonAuthFilters.scala similarity index 98% rename from common/app/http/GoogleAuthFilters.scala rename to common/app/http/CommonAuthFilters.scala index 1a8740cea20b..f918a4891fcd 100644 --- a/common/app/http/GoogleAuthFilters.scala +++ b/common/app/http/CommonAuthFilters.scala @@ -11,7 +11,7 @@ import play.api.mvc.{Filter, RequestHeader, Result} import scala.concurrent.Future -object GoogleAuthFilters { +object CommonAuthFilters { val LOGIN_ORIGIN_KEY = "loginOriginUrl" class AuthFilterWithExemptions(loginUrl: FilterExemption, exemptions: Seq[FilterExemption])(implicit diff --git a/preview/app/http/PreviewFilters.scala b/preview/app/http/PreviewFilters.scala index caee6160d108..2e888400bfbf 100644 --- a/preview/app/http/PreviewFilters.scala +++ b/preview/app/http/PreviewFilters.scala @@ -1,9 +1,8 @@ package http import org.apache.pekko.stream.Materializer -import GoogleAuthFilters.AuthFilterWithExemptions +import CommonAuthFilters.AuthFilterWithExemptions import controllers.HealthCheck -import googleAuth.FilterExemptions import model.ApplicationContext import play.api.http.{HttpConfiguration, HttpFilters} import play.api.mvc.{Filter, RequestHeader, Result} From 79770862a97e12ec6b394ba4084ff0169bee8f3b Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Mon, 15 Apr 2024 13:31:58 +0100 Subject: [PATCH 3/3] change authentication for `preview` and `admin` to `pan-domain-authentication` from plain Google auth --- .prout.json | 2 +- admin/app/AppLoader.scala | 39 ++++- admin/app/controllers/AdminControllers.scala | 3 +- admin/app/controllers/HealthCheck.scala | 17 +-- .../controllers/admin/IndexController.scala | 17 --- .../admin/OAuthLoginAdminController.scala | 34 ----- .../admin/SwitchboardController.scala | 12 +- .../cache/ImageDecacheController.scala | 21 +-- .../cache/PageDecacheController.scala | 20 +-- admin/app/http/AdminFilters.scala | 28 ---- admin/app/views/admin_head.scala.html | 15 +- admin/app/views/admin_main.scala.html | 2 +- admin/app/views/auth/login.scala.html | 11 -- admin/conf/routes | 5 +- admin/test/TestAppLoader.scala | 18 ++- build.sbt | 4 +- common/app/conf/GoogleAuth.scala | 52 ------- common/app/googleAuth/FilterExemptions.scala | 15 -- .../app/googleAuth/OAuthLoginController.scala | 139 ------------------ common/app/http/CommonAuthFilters.scala | 58 -------- .../app/http/GuardianAuthWithExemptions.scala | 91 ++++++++++++ dev-build/conf/routes | 5 +- preview/app/AppLoader.scala | 27 +++- .../OAuthLoginPreviewController.scala | 31 ---- .../PreviewContentSecurityPolicyFilter.scala | 19 +++ preview/app/http/PreviewFilters.scala | 48 ------ preview/app/http/PreviewNoCacheFilter.scala | 13 ++ preview/app/views/previewAuth.scala.html | 51 ------- preview/conf/routes | 6 +- preview/public/css/style.css | 39 +---- project/Dependencies.scala | 4 +- 31 files changed, 241 insertions(+), 605 deletions(-) delete mode 100644 admin/app/controllers/admin/OAuthLoginAdminController.scala delete mode 100644 admin/app/http/AdminFilters.scala delete mode 100644 admin/app/views/auth/login.scala.html delete mode 100644 common/app/conf/GoogleAuth.scala delete mode 100644 common/app/googleAuth/FilterExemptions.scala delete mode 100644 common/app/googleAuth/OAuthLoginController.scala delete mode 100644 common/app/http/CommonAuthFilters.scala create mode 100644 common/app/http/GuardianAuthWithExemptions.scala delete mode 100644 preview/app/controllers/OAuthLoginPreviewController.scala create mode 100644 preview/app/http/PreviewContentSecurityPolicyFilter.scala delete mode 100644 preview/app/http/PreviewFilters.scala create mode 100644 preview/app/http/PreviewNoCacheFilter.scala delete mode 100644 preview/app/views/previewAuth.scala.html diff --git a/.prout.json b/.prout.json index 7e870171668d..51667e6d8eff 100644 --- a/.prout.json +++ b/.prout.json @@ -8,7 +8,7 @@ } }, "ADMIN-PROD": { - "url": "https://frontend.gutools.co.uk/login", + "url": "https://frontend.gutools.co.uk/_healthcheck", "overdue": "30M", "messages": { "seen": "prout/seen.md" diff --git a/admin/app/AppLoader.scala b/admin/app/AppLoader.scala index cf41722fb59b..7d490b9d4ae9 100644 --- a/admin/app/AppLoader.scala +++ b/admin/app/AppLoader.scala @@ -8,10 +8,12 @@ import conf.switches.SwitchboardLifecycle import conf.CachedHealthCheckLifeCycle import controllers.{AdminControllers, HealthCheck} import _root_.dfp.DfpDataCacheLifecycle +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.AmazonS3ClientBuilder import org.apache.pekko.actor.{ActorSystem => PekkoActorSystem} import concurrent.BlockingOperations import contentapi.{CapiHttpClient, ContentApiClient, HttpClient} -import http.{AdminFilters, AdminHttpErrorHandler, CommonGzipFilter} +import http.{AdminHttpErrorHandler, CommonGzipFilter, Filters, GuardianAuthWithExemptions, routes} import dev.DevAssetsController import jobs._ import model.{AdminLifecycle, ApplicationIdentity} @@ -25,6 +27,7 @@ import play.api.i18n.I18nComponents import play.api.libs.ws.WSClient import services.{ParameterStoreService, _} import router.Routes +import conf.Configuration.aws.mandatoryCredentials import scala.concurrent.ExecutionContext @@ -76,6 +79,35 @@ trait AdminServices extends I18nComponents { trait AppComponents extends FrontendComponents with AdminControllers with AdminServices { + private lazy val s3Client = AmazonS3ClientBuilder + .standard() + .withRegion(Regions.EU_WEST_1) + .withCredentials( + mandatoryCredentials, + ) + .build() + + lazy val auth = new GuardianAuthWithExemptions( + controllerComponents, + wsClient, + toolsDomainPrefix = "frontend", + oauthCallbackPath = routes.GuardianAuthWithExemptions.oauthCallback.path, + s3Client, + system = "frontend-admin", + extraDoNotAuthenticatePathPrefixes = Seq( + "/deploys", //not authenticated so it can be accessed by Prout to determine which builds have been deployed + "/deploy", //not authenticated so it can be accessed by Riff-Raff to notify about a new build being deployed + // Date: 06 July 2021 + // Author: Pascal + // Added as part of posing the ground for the interactive migration. + // It should be removed when the Interactives migration is complete, meaning when we no longer need the routes + // POST /interactive-librarian/live-presser/*path + // POST /interactive-librarian/read-clean-write/*path + // in [admin]. + "/interactive-librarian/", + ), + ) + lazy val healthCheck = wire[HealthCheck] lazy val devAssetsController = wire[DevAssetsController] lazy val logbackOperationsPool = wire[LogbackOperationsPool] @@ -88,7 +120,6 @@ trait AppComponents extends FrontendComponents with AdminControllers with AdminS wire[SurgingContentAgentLifecycle], wire[DfpAgentLifecycle], wire[DfpDataCacheLifecycle], - wire[CachedHealthCheckLifeCycle], wire[CommercialDfpReportingLifecycle], ) @@ -103,6 +134,8 @@ trait AppComponents extends FrontendComponents with AdminControllers with AdminS def pekkoActorSystem: PekkoActorSystem + override lazy val httpFilters: Seq[EssentialFilter] = + auth.filter :: Filters.common(frontend.admin.BuildInfo) ++ wire[CommonGzipFilter].filters + override lazy val httpErrorHandler: HttpErrorHandler = wire[AdminHttpErrorHandler] - override lazy val httpFilters: Seq[EssentialFilter] = wire[CommonGzipFilter].filters ++ wire[AdminFilters].filters } diff --git a/admin/app/controllers/AdminControllers.scala b/admin/app/controllers/AdminControllers.scala index 1e11c157a3e7..8f64abc5a8bb 100644 --- a/admin/app/controllers/AdminControllers.scala +++ b/admin/app/controllers/AdminControllers.scala @@ -5,6 +5,7 @@ import controllers.admin._ import controllers.admin.commercial._ import controllers.cache.{ImageDecacheController, PageDecacheController} import dfp._ +import http.GuardianAuthWithExemptions import model.ApplicationContext import play.api.http.HttpConfiguration import play.api.libs.ws.WSClient @@ -37,8 +38,8 @@ trait AdminControllers { def placementService: PlacementService def dfpApi: DfpApi def parameterStoreService: ParameterStoreService + def auth: GuardianAuthWithExemptions - lazy val oAuthLoginController = wire[OAuthLoginAdminController] lazy val uncachedWebAssets = wire[UncachedWebAssets] lazy val uncachedAssets = wire[UncachedAssets] lazy val adminIndexController = wire[AdminIndexController] diff --git a/admin/app/controllers/HealthCheck.scala b/admin/app/controllers/HealthCheck.scala index 9bad0269aeeb..5000acb85ac2 100644 --- a/admin/app/controllers/HealthCheck.scala +++ b/admin/app/controllers/HealthCheck.scala @@ -1,13 +1,12 @@ package controllers -import conf.{AllGoodCachedHealthCheck, NeverExpiresSingleHealthCheck} -import play.api.libs.ws.WSClient -import play.api.mvc.ControllerComponents +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} -import scala.concurrent.ExecutionContext +class HealthCheck(val controllerComponents: ControllerComponents) extends BaseController { -class HealthCheck(wsClient: WSClient, val controllerComponents: ControllerComponents)(implicit - executionContext: ExecutionContext, -) extends AllGoodCachedHealthCheck( - NeverExpiresSingleHealthCheck("/login"), - )(wsClient, executionContext) + def healthCheck(): Action[AnyContent] = + Action { + Ok("OK") + } + +} diff --git a/admin/app/controllers/admin/IndexController.scala b/admin/app/controllers/admin/IndexController.scala index 189f50702dd8..9a795f4c3574 100644 --- a/admin/app/controllers/admin/IndexController.scala +++ b/admin/app/controllers/admin/IndexController.scala @@ -1,25 +1,8 @@ package controllers.admin -import com.gu.googleauth.AuthAction -import conf.AdminConfiguration import model.{ApplicationContext, NoCache} -import play.api.http.HttpConfiguration import play.api.mvc._ -trait AdminAuthController { - - def controllerComponents: ControllerComponents - - case class AdminAuthAction(httpConfiguration: HttpConfiguration) - extends AuthAction( - conf - .GoogleAuth(None, httpConfiguration, AdminConfiguration.oauthCredentialsWithSingleCallBack(None)) - .getConfigOrDie, - routes.OAuthLoginAdminController.login, - controllerComponents.parsers.default, - )(controllerComponents.executionContext) -} - class AdminIndexController(val controllerComponents: ControllerComponents)(implicit context: ApplicationContext) extends BaseController { diff --git a/admin/app/controllers/admin/OAuthLoginAdminController.scala b/admin/app/controllers/admin/OAuthLoginAdminController.scala deleted file mode 100644 index 948ce4c4a695..000000000000 --- a/admin/app/controllers/admin/OAuthLoginAdminController.scala +++ /dev/null @@ -1,34 +0,0 @@ -package controllers.admin - -import com.gu.googleauth.GoogleAuthConfig -import conf.AdminConfiguration -import googleAuth.OAuthLoginController -import model.ApplicationContext -import play.api.http.HttpConfiguration -import play.api.libs.ws.WSClient -import play.api.mvc.{Action, AnyContent, ControllerComponents, Request} - -class OAuthLoginAdminController( - val wsClient: WSClient, - val httpConfiguration: HttpConfiguration, - val controllerComponents: ControllerComponents, -)(implicit context: ApplicationContext) - extends OAuthLoginController { - - override def login: Action[AnyContent] = - Action { implicit request => - val error = request.flash.get("error") - Ok(views.html.auth.login(error)) - } - override def googleAuthConfig(request: Request[AnyContent]): Option[GoogleAuthConfig] = { - val currentHost = Some(s"${if (request.secure) "https" else "http"}://${request.host}") - - conf - .GoogleAuth( - currentHost, - httpConfiguration, - AdminConfiguration.oauthCredentialsWithSingleCallBack(currentHost), - ) - .config - } -} diff --git a/admin/app/controllers/admin/SwitchboardController.scala b/admin/app/controllers/admin/SwitchboardController.scala index 927a84eac584..84f45291fb50 100644 --- a/admin/app/controllers/admin/SwitchboardController.scala +++ b/admin/app/controllers/admin/SwitchboardController.scala @@ -1,9 +1,9 @@ package controllers.admin -import com.gu.googleauth.UserIdentity import common._ import conf.Configuration import conf.switches.Switches +import http.GuardianAuthWithExemptions import model.{ApplicationContext, NoCache} import play.api.mvc._ import services.SwitchNotification @@ -11,7 +11,11 @@ import tools.Store import scala.concurrent.Future -class SwitchboardController(pekkoAsync: PekkoAsync, val controllerComponents: ControllerComponents)(implicit +class SwitchboardController( + pekkoAsync: PekkoAsync, + auth: GuardianAuthWithExemptions, + val controllerComponents: ControllerComponents, +)(implicit context: ApplicationContext, ) extends BaseController with GuLogging @@ -56,7 +60,9 @@ class SwitchboardController(pekkoAsync: PekkoAsync, val controllerComponents: Co } else { log.info("saving switchboard") - val requester: String = UserIdentity.fromRequest(request) map (_.fullName) getOrElse "unknown user (dev-build?)" + val requester: String = + auth.readAuthenticatedUser(request) map (authed => s"${authed.user.firstName} ${authed.user.lastName}", + ) getOrElse "unknown user (dev-build?)" val updates: Seq[String] = request.body.asFormUrlEncoded.map { params => Switches.all map { switch => switch.name + "=" + params.get(switch.name).map(v => "on").getOrElse("off") diff --git a/admin/app/controllers/cache/ImageDecacheController.scala b/admin/app/controllers/cache/ImageDecacheController.scala index efa6742354f4..40ee5098f87d 100644 --- a/admin/app/controllers/cache/ImageDecacheController.scala +++ b/admin/app/controllers/cache/ImageDecacheController.scala @@ -1,28 +1,22 @@ package controllers.cache -import java.net.URI -import java.util.UUID -import com.gu.googleauth.UserIdentity import common.{GuLogging, ImplicitControllerExecutionContext} -import controllers.admin.AdminAuthController import model.{ApplicationContext, NoCache} -import play.api.http.HttpConfiguration import play.api.libs.ws.{WSClient, WSResponse} -import play.api.mvc.Security.AuthenticatedRequest import play.api.mvc._ +import java.net.URI +import java.util.UUID import scala.concurrent.Future import scala.concurrent.Future.successful class ImageDecacheController( wsClient: WSClient, val controllerComponents: ControllerComponents, - val httpConfiguration: HttpConfiguration, )(implicit context: ApplicationContext) extends BaseController with GuLogging - with ImplicitControllerExecutionContext - with AdminAuthController { + with ImplicitControllerExecutionContext { import ImageDecacheController._ private val iGuim = """i.(guim|guimcode).co.uk/img/(static|media|uploads|sport)(/.*)""".r @@ -34,9 +28,8 @@ class ImageDecacheController( } def decache(): Action[AnyContent] = - AdminAuthAction(httpConfiguration).async { implicit request => - getSubmittedImage(request) - .map(new URI(_)) + Action.async { implicit request => + getSubmittedImageURI(request) .map { imageUri => // here we limit the url to ones for which purging is supported val originUrl: String = s"${imageUri.getHost}${imageUri.getPath}" match { @@ -84,15 +77,15 @@ class ImageDecacheController( } .getOrElse(successful(BadRequest("No image submitted"))) - } - private def getSubmittedImage(request: AuthenticatedRequest[AnyContent, UserIdentity]): Option[String] = + private def getSubmittedImageURI(request: Request[AnyContent]): Option[URI] = request.body.asFormUrlEncoded .getOrElse(Map.empty) .get("url") .flatMap(_.headOption) .map(_.trim) + .map(new URI(_)) } diff --git a/admin/app/controllers/cache/PageDecacheController.scala b/admin/app/controllers/cache/PageDecacheController.scala index 9f3ad71fdbdd..7e7a0fdbecaf 100644 --- a/admin/app/controllers/cache/PageDecacheController.scala +++ b/admin/app/controllers/cache/PageDecacheController.scala @@ -1,32 +1,24 @@ package controllers.cache import java.net.URI -import com.gu.googleauth.UserIdentity import common.{GuLogging, ImplicitControllerExecutionContext} -import controllers.admin.AdminAuthController import model.{ApplicationContext, NoCache} import org.apache.commons.codec.digest.DigestUtils -import play.api.http.HttpConfiguration import play.api.libs.ws.WSClient -import play.api.mvc.Security.AuthenticatedRequest import play.api.mvc._ import purge.{AjaxHost, CdnPurge, GuardianHost} import scala.concurrent.Future import scala.concurrent.Future.successful -case class PrePurgeTestResult(url: String, passed: Boolean) - class PageDecacheController( wsClient: WSClient, val controllerComponents: ControllerComponents, - val httpConfiguration: HttpConfiguration, )(implicit context: ApplicationContext, ) extends BaseController with GuLogging - with ImplicitControllerExecutionContext - with AdminAuthController { + with ImplicitControllerExecutionContext { def renderPageDecache(): Action[AnyContent] = Action.async { implicit request => @@ -39,7 +31,7 @@ class PageDecacheController( } def decacheAjax(): Action[AnyContent] = - AdminAuthAction(httpConfiguration).async { implicit request => + Action.async { implicit request => getSubmittedUrlPathMd5(request) match { case Some(path) => CdnPurge.soft(wsClient, path, AjaxHost).map(message => NoCache(Ok(views.html.cache.ajaxDecache(message)))) @@ -48,7 +40,7 @@ class PageDecacheController( } def decachePage(): Action[AnyContent] = - AdminAuthAction(httpConfiguration).async { implicit request => + Action.async { implicit request => getSubmittedUrlPathMd5(request) match { case Some(md5Path) => CdnPurge @@ -58,13 +50,15 @@ class PageDecacheController( } } - private def getSubmittedUrlPathMd5(request: AuthenticatedRequest[AnyContent, UserIdentity]): Option[String] = { + private def getSubmittedUrlPathMd5(request: Request[AnyContent]): Option[String] = { request.body.asFormUrlEncoded .getOrElse(Map.empty) .get("url") .flatMap(_.headOption) .map(_.trim) - .map(url => DigestUtils.md5Hex(new URI(url).getPath)) + .map(new URI(_)) + .map(_.getPath) + .map(DigestUtils.md5Hex) } } diff --git a/admin/app/http/AdminFilters.scala b/admin/app/http/AdminFilters.scala deleted file mode 100644 index b9cf11b4a333..000000000000 --- a/admin/app/http/AdminFilters.scala +++ /dev/null @@ -1,28 +0,0 @@ -package http - -import org.apache.pekko.stream.Materializer -import CommonAuthFilters.AuthFilterWithExemptions -import model.ApplicationContext -import play.api.http.{HttpConfiguration, HttpFilters} -import play.api.mvc.EssentialFilter - -import scala.concurrent.ExecutionContext - -class AdminFilters(httpConfiguration: HttpConfiguration)(implicit - mat: Materializer, - applicationContext: ApplicationContext, - executionContext: ExecutionContext, -) extends HttpFilters { - - val filterExemptions = FilterExemptions( - "/deploys", //not authenticated so it can be accessed by Prout to determine which builds have been deployed - "/deploy", //not authenticated so it can be accessed by Riff-Raff to notify about a new build being deployed - ) - val adminAuthFilter = new AuthFilterWithExemptions(filterExemptions.loginExemption, filterExemptions.exemptions)( - mat, - applicationContext, - httpConfiguration, - ) - - val filters: List[EssentialFilter] = adminAuthFilter :: Filters.common(frontend.admin.BuildInfo) -} diff --git a/admin/app/views/admin_head.scala.html b/admin/app/views/admin_head.scala.html index 137fbb63236e..b2127e0e9fd7 100644 --- a/admin/app/views/admin_head.scala.html +++ b/admin/app/views/admin_head.scala.html @@ -1,6 +1,4 @@ -@( - isAuthed: Boolean = false -)(implicit request: RequestHeader, context: model.ApplicationContext) +@()(implicit request: RequestHeader, context: model.ApplicationContext) @import conf.Configuration @@ -13,16 +11,5 @@ - @if(isAuthed){ - - } else { - - } diff --git a/admin/app/views/admin_main.scala.html b/admin/app/views/admin_main.scala.html index cda15a4fda4b..9924b7117be5 100644 --- a/admin/app/views/admin_main.scala.html +++ b/admin/app/views/admin_main.scala.html @@ -56,7 +56,7 @@ - @admin_head(isAuthed) + @admin_head()
@content diff --git a/admin/app/views/auth/login.scala.html b/admin/app/views/auth/login.scala.html deleted file mode 100644 index 0960bc988a04..000000000000 --- a/admin/app/views/auth/login.scala.html +++ /dev/null @@ -1,11 +0,0 @@ -@(error: Option[String] = None)(implicit request: RequestHeader, context: model.ApplicationContext) - -@admin_main("Login") { - @if(error.isDefined) { -
-

@error.get

-
- } else { - - } -} diff --git a/admin/conf/routes b/admin/conf/routes index 3e5983c55222..32bace69d8cb 100644 --- a/admin/conf/routes +++ b/admin/conf/routes @@ -6,10 +6,7 @@ GET /_healthcheck controllers.HealthCheck.healthCheck() # authentication endpoints -GET /login controllers.admin.OAuthLoginAdminController.login -POST /login controllers.admin.OAuthLoginAdminController.loginAction -GET /oauth2callback controllers.admin.OAuthLoginAdminController.oauth2Callback -GET /logout controllers.admin.OAuthLoginAdminController.logout +GET /oauthCallback http.GuardianAuthWithExemptions.oauthCallback # static files GET /assets/admin/lib/*file controllers.admin.UncachedWebAssets.at(file) diff --git a/admin/test/TestAppLoader.scala b/admin/test/TestAppLoader.scala index b83c543c3a8c..baedca1f2ae8 100644 --- a/admin/test/TestAppLoader.scala +++ b/admin/test/TestAppLoader.scala @@ -1,6 +1,15 @@ import app.FrontendComponents import play.api.ApplicationLoader.Context import play.api.BuiltInComponentsFromContext +import play.api.mvc.EssentialFilter +import org.scalatestplus.mockito.MockitoSugar +import org.mockito.Mockito._ +import http.GuardianAuthWithExemptions +import org.apache.pekko.stream.Materializer +import org.mockito.stubbing.Answer +import play.api.mvc.{Filter, _} + +import scala.concurrent.Future trait TestComponents { self: AppComponents => @@ -11,5 +20,12 @@ trait TestComponents { class TestAppLoader extends AppLoader { override def buildComponents(context: Context): FrontendComponents = - new BuiltInComponentsFromContext(context) with AppComponents with TestComponents + new BuiltInComponentsFromContext(context) with AppComponents with TestComponents with MockitoSugar { + private val mockAuth = mock[GuardianAuthWithExemptions] + when(mockAuth.filter).thenReturn(new Filter { + implicit val mat: Materializer = materializer + def apply(nextFilter: RequestHeader => Future[Result])(request: RequestHeader) = nextFilter(request) // no auth + }) + override lazy val auth = mockAuth + } } diff --git a/build.sbt b/build.sbt index 6682c0def5bc..de4980efb9e1 100644 --- a/build.sbt +++ b/build.sbt @@ -38,9 +38,7 @@ val common = library("common") jodaTime, jSoup, json4s, - playGoogleAuth, - playSecretRotation, - playSecretRotationAwsSdk, + panDomainAuth, quartzScheduler, redisClient, rome, diff --git a/common/app/conf/GoogleAuth.scala b/common/app/conf/GoogleAuth.scala deleted file mode 100644 index dc403615641f..000000000000 --- a/common/app/conf/GoogleAuth.scala +++ /dev/null @@ -1,52 +0,0 @@ -package conf - -import conf.Configuration.OAuthCredentials -import com.amazonaws.auth.AWSCredentialsProviderChain -import com.amazonaws.regions.Regions -import com.amazonaws.services.simplesystemsmanagement.AWSSimpleSystemsManagementClientBuilder -import com.gu.googleauth.{AntiForgeryChecker, GoogleAuthConfig} -import com.gu.play.secretrotation.{SnapshotProvider, TransitionTiming} -import play.api.http.HttpConfiguration - -import java.time.Duration.{ofHours, ofMinutes} - -case class GoogleAuth( - currentHost: Option[String], - httpConfiguration: HttpConfiguration, - oauthCredentials: Option[OAuthCredentials], -) { - private val frontendCredentialsProvider = new AWSCredentialsProviderChain( - Configuration.aws.mandatoryCredentials, - ) - private val ssmClient = AWSSimpleSystemsManagementClientBuilder - .standard() - .withCredentials(frontendCredentialsProvider) - .withRegion(Regions.EU_WEST_1) - .build() - - val secretStateSupplier: SnapshotProvider = { - import com.gu.play.secretrotation.aws.parameterstore - - new parameterstore.SecretSupplier( - TransitionTiming(usageDelay = ofMinutes(3), overlapDuration = ofHours(2)), - Configuration.googleOAuth.playAppSecretParameterName, - parameterstore.AwsSdkV1(ssmClient), - ) - } - - val config = oauthCredentials.map { cred => - GoogleAuthConfig( - cred.oauthClientId, // The client ID from the dev console - cred.oauthSecret, // The client secret from the dev console - cred.oauthCallback, // The redirect URL Google send users back to (must be the same as that configured in the developer console) - List("guardian.co.uk"), // Google App domain to restrict login - antiForgeryChecker = - AntiForgeryChecker(secretStateSupplier, AntiForgeryChecker.signatureAlgorithmFromPlay(httpConfiguration)), - ) - } - - def getConfigOrDie: GoogleAuthConfig = - config getOrElse { - throw new RuntimeException("You must set up credentials for Google Auth") - } -} diff --git a/common/app/googleAuth/FilterExemptions.scala b/common/app/googleAuth/FilterExemptions.scala deleted file mode 100644 index dde6e3281e5a..000000000000 --- a/common/app/googleAuth/FilterExemptions.scala +++ /dev/null @@ -1,15 +0,0 @@ -package googleAuth - -import com.gu.googleauth.FilterExemption - -case class FilterExemptions(additionalUrls: String*) { - - lazy val loginExemption: FilterExemption = FilterExemption("/login") - lazy val exemptions: Seq[FilterExemption] = List( - // Default - FilterExemption("/oauth2callback"), - FilterExemption("/assets"), - FilterExemption("/favicon.ico"), - FilterExemption("/_healthcheck"), - ) ++ additionalUrls.map { url => FilterExemption(url) } -} diff --git a/common/app/googleAuth/OAuthLoginController.scala b/common/app/googleAuth/OAuthLoginController.scala deleted file mode 100644 index ccc5e83d6ce5..000000000000 --- a/common/app/googleAuth/OAuthLoginController.scala +++ /dev/null @@ -1,139 +0,0 @@ -package googleAuth - -import com.gu.googleauth.{GoogleAuth, GoogleAuthConfig, UserIdentity} -import common.{Crypto, ImplicitControllerExecutionContext, GuLogging} -import conf.Configuration -import org.joda.time.DateTime -import play.api.http.HttpConfiguration -import play.api.libs.json.Json -import play.api.libs.ws.WSClient -import play.api.mvc._ - -import scala.concurrent.Future - -trait OAuthLoginController extends BaseController with ImplicitControllerExecutionContext with implicits.Requests { - - implicit def wsClient: WSClient - def login: Action[AnyContent] - def googleAuthConfig(request: Request[AnyContent]): Option[GoogleAuthConfig] - def httpConfiguration: HttpConfiguration - - val authCookie = new AuthCookie(httpConfiguration) - - val LOGIN_ORIGIN_KEY = "loginOriginUrl" - val ANTI_FORGERY_KEY = "play-googleauth-session-id" - val forbiddenNoCredentials = Forbidden("Invalid OAuth credentials set") - lazy val validRedirectDomain = """^(\w+:\/\/[^/]+\.(?:dev-)?gutools\.co\.uk\/.*)$""".r - - /* - Redirect to Google with anti forgery token (that we keep in session storage - note that flashing is NOT secure) - */ - def loginAction: Action[AnyContent] = - Action.async { implicit request => - googleAuthConfig(request) - .flatMap(overrideRedirectUrl) - .map { config => - config.antiForgeryChecker.ensureUserHasSessionId { sessionId => - GoogleAuth.redirectToGoogle(config, sessionId) - } - } - .getOrElse(Future.successful(forbiddenNoCredentials)) - } - - // Allow redirect url to be overridden via request params. Only allows gutools.co.uk - private def overrideRedirectUrl(config: GoogleAuthConfig)(implicit request: RequestHeader) = { - request.getQueryString("redirect-url") match { - case Some(validRedirectDomain(url)) => Some(config.copy(redirectUrl = url)) - case None => Some(config) - case _ => None - } - } - - /* - User comes back from Google. - We must ensure we have the anti forgery token from the loginAction call and pass this into a verification call which - will return a Future[UserIdentity] if the authentication is successful. If unsuccessful then the Future will fail. - */ - def oauth2Callback: Action[AnyContent] = - Action.async { implicit request => - googleAuthConfig(request) - .flatMap(overrideRedirectUrl) - .map { config => - request.session.get(ANTI_FORGERY_KEY) match { - case None => - Future.successful( - Redirect("/login").withNewSession - .flashing("error" -> "Anti forgery token missing in session"), - ) - case Some(token) => - GoogleAuth - .validatedUserIdentity(config) - // drop the avatarUrl as it can be very large and we don't use it anywhere - .map(_.copy(avatarUrl = None)) - .map { userIdentity: UserIdentity => - // We store the URL a user was trying to get to in the LOGIN_ORIGIN_KEY in AuthAction - // Redirect a user back there now if it exists - val redirect = request.session.get(LOGIN_ORIGIN_KEY) match { - case Some(url) => Redirect(url) - case None => Redirect("/") - } - // Store the JSON representation of the identity in the session - this is checked by AuthAction later - val sessionAdd: Seq[(String, String)] = Seq( - Option((UserIdentity.KEY, Json.toJson(userIdentity).toString())), - Option((Configuration.cookies.lastSeenKey, DateTime.now.toString())), - ).flatten - - val result = redirect - .addingToSession(sessionAdd: _*) - .removingFromSession(ANTI_FORGERY_KEY, LOGIN_ORIGIN_KEY) - - authCookie - .from(userIdentity) - .map(authCookie => result.withCookies(authCookie)) - .getOrElse(result) - } recover { - case t => - // you might want to record login failures here - we just redirect to the login page - Redirect("/login") - .withSession(request.session - ANTI_FORGERY_KEY) - .flashing("error" -> s"Login failure: ${t.toString}") - } - } - } - .getOrElse(Future.successful(forbiddenNoCredentials)) - } - - def logout: Action[AnyContent] = - Action { implicit request => - Redirect("/login").withNewSession - } -} - -class AuthCookie(httpConfiguration: HttpConfiguration) extends GuLogging { - - private val cookieName = "GU_PV_AUTH" - private val oneDayInSeconds: Int = 86400 - - def from(id: UserIdentity): Option[Cookie] = { - val idWith30DayExpiry = id.copy(exp = (System.currentTimeMillis() / 1000) + oneDayInSeconds) - Some( - Cookie( - cookieName, - Crypto.encryptAES(Json.toJson(idWith30DayExpiry).toString, httpConfiguration.secret.secret), - Some(oneDayInSeconds), - ), - ) - } - - def toUserIdentity(request: RequestHeader): Option[UserIdentity] = { - try { - request.cookies.get(cookieName).flatMap { cookie => - UserIdentity.fromJson(Json.parse(Crypto.decryptAES(cookie.value, httpConfiguration.secret.secret))) - } - } catch { - case e: Exception => - log.error("Could not parse Auth Cookie", e) - None - } - } -} diff --git a/common/app/http/CommonAuthFilters.scala b/common/app/http/CommonAuthFilters.scala deleted file mode 100644 index f918a4891fcd..000000000000 --- a/common/app/http/CommonAuthFilters.scala +++ /dev/null @@ -1,58 +0,0 @@ -package http - -import org.apache.pekko.stream.Materializer -import com.gu.googleauth.{FilterExemption, UserIdentity} -import googleAuth.AuthCookie -import model.ApplicationContext -import play.api.Mode -import play.api.http.HttpConfiguration -import play.api.mvc.Results.Redirect -import play.api.mvc.{Filter, RequestHeader, Result} - -import scala.concurrent.Future - -object CommonAuthFilters { - val LOGIN_ORIGIN_KEY = "loginOriginUrl" - - class AuthFilterWithExemptions(loginUrl: FilterExemption, exemptions: Seq[FilterExemption])(implicit - val mat: Materializer, - context: ApplicationContext, - httpConfiguration: HttpConfiguration, - ) extends Filter { - - val authCookie = new AuthCookie(httpConfiguration) - - // Date: 06 July 2021 - // Author: Pascal - - // Condition [1], below, was added in July 2021, as part of posing the ground for the interactive migration. - // It should be removed when the Interactives migration is complete, meaning when we no longer need the routes - // POST /interactive-librarian/live-presser/*path - // POST /interactive-librarian/read-clean-write/*path - // in [admin]. - // Note that a slightly better solution would have been to set up a new entry in AdminFilters's FilterExemptions - // but they do not interpret wildcards. As a consequence the next best solution is to add a migration specific - // clause to doNotAuthenticate - - private def doNotAuthenticate(request: RequestHeader) = - context.environment.mode == Mode.Test || - request.path.startsWith(loginUrl.path) || - request.path.startsWith("/interactive-librarian/") || // Condition [1] - exemptions.exists(exemption => request.path.startsWith(exemption.path)) - - def apply(nextFilter: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = { - if (doNotAuthenticate(request)) { - nextFilter(request) - } else { - authCookie.toUserIdentity(request).filter(_.isValid).orElse(UserIdentity.fromRequest(request)) match { - case Some(identity) if identity.isValid => nextFilter(request) - case _ => - Future.successful( - Redirect(loginUrl.path) - .addingToSession((LOGIN_ORIGIN_KEY, request.uri))(request), - ) - } - } - } - } -} diff --git a/common/app/http/GuardianAuthWithExemptions.scala b/common/app/http/GuardianAuthWithExemptions.scala new file mode 100644 index 000000000000..9b3655fab351 --- /dev/null +++ b/common/app/http/GuardianAuthWithExemptions.scala @@ -0,0 +1,91 @@ +package http + +import com.amazonaws.services.s3.AmazonS3 +import com.gu.pandomainauth.action.AuthActions +import com.gu.pandomainauth.model.AuthenticatedUser +import com.gu.pandomainauth.{PanDomain, PanDomainAuthSettingsRefresher} +import common.Environment.stage +import model.ApplicationContext +import org.apache.pekko.stream.Materializer +import play.api.Mode +import play.api.libs.ws.WSClient +import play.api.mvc.{BaseController, _} + +import java.net.URL +import scala.concurrent.Future + +class GuardianAuthWithExemptions( + override val controllerComponents: ControllerComponents, + override val wsClient: WSClient, + toolsDomainPrefix: String, + oauthCallbackPath: String, + s3Client: AmazonS3, + system: String, + extraDoNotAuthenticatePathPrefixes: Seq[String], +)(implicit + val mat: Materializer, + context: ApplicationContext, +) extends AuthActions + with BaseController { + + private val outer = this + + private def toolsDomainSuffix = + stage match { + case "PROD" => "gutools.co.uk" + case "CODE" => "code.dev-gutools.co.uk" + case _ => s"local.dev-gutools.co.uk" // covers DEV, LOCAL, tests etc. + } + + override def panDomainSettings = + new PanDomainAuthSettingsRefresher( + domain = toolsDomainSuffix, + system, + bucketName = "pan-domain-auth-settings", + settingsFileKey = s"$toolsDomainSuffix.settings", + s3Client, + ) + + override def authCallbackUrl = s"https://$toolsDomainPrefix.$toolsDomainSuffix$oauthCallbackPath" + + override def validateUser(authedUser: AuthenticatedUser): Boolean = { + PanDomain.guardianValidation(authedUser) + } + + /** + * By default, the user validation method is called every request. If your validation + * method has side-effects or is expensive (perhaps hitting a database), setting this + * to true will ensure that validateUser is only called when the OAuth session is refreshed + */ + override def cacheValidation = false + + def oauthCallback: Action[AnyContent] = + Action.async { implicit request => + processOAuthCallback() + } + + val filter: Filter = new Filter { + + override val mat: Materializer = outer.mat + + private def doNotAuthenticate(request: RequestHeader) = + context.environment.mode == Mode.Test || + (List( + new URL(authCallbackUrl).getPath, // oauth callback + "/assets", + "/favicon.ico", + "/_healthcheck", + ) ++ extraDoNotAuthenticatePathPrefixes).exists(request.path.startsWith) + + def apply(nextFilter: RequestHeader => Future[Result])(request: RequestHeader): Future[Result] = { + if (doNotAuthenticate(request)) { + nextFilter(request) + } else { + // TODO: in future PR add a permission check here based on user, likely via a function passed in to GuardianAuthWithExemptions + AuthAction.authenticateRequest(request) { user => + nextFilter(request) + } + } + } + } +} diff --git a/dev-build/conf/routes b/dev-build/conf/routes index d87460931946..2ee7de3ec66d 100644 --- a/dev-build/conf/routes +++ b/dev-build/conf/routes @@ -191,10 +191,7 @@ GET /sport/rugby/api/score/:year/:month/:day/:team1Id/:team2Id.json GET /sport/rugby/api/score/:year/:month/:day/:team1Id/:team2Id rugby.controllers.MatchesController.score(year, month, day, team1Id, team2Id) # Admin -GET /login controllers.admin.OAuthLoginAdminController.login -POST /login controllers.admin.OAuthLoginAdminController.loginAction -GET /oauth2callback controllers.admin.OAuthLoginAdminController.oauth2Callback -GET /logout controllers.admin.OAuthLoginAdminController.logout +GET /oauthCallback http.GuardianAuthWithExemptions.oauthCallback GET /admin controllers.admin.AdminIndexController.admin() GET /config controllers.AppConfigController.renderAppConfig() GET /config/parameter/*key controllers.AppConfigController.findParameter(key: String) diff --git a/preview/app/AppLoader.scala b/preview/app/AppLoader.scala index 9819fd3b1a45..69508ff2056d 100644 --- a/preview/app/AppLoader.scala +++ b/preview/app/AppLoader.scala @@ -1,6 +1,8 @@ import agents.MostViewedAgent import org.apache.pekko.actor.{ActorSystem => PekkoActorSystem} import app.{FrontendApplicationLoader, FrontendComponents, LifecycleComponent} +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.AmazonS3ClientBuilder import com.softwaremill.macwire._ import commercial.CommercialLifecycle import commercial.controllers.CommercialControllers @@ -17,7 +19,7 @@ import cricket.controllers.CricketControllers import dev.DevAssetsController import feed.OnwardJourneyLifecycle import football.controllers.FootballControllers -import http.PreviewFilters +import http.{Filters, GuardianAuthWithExemptions, PreviewContentSecurityPolicyFilter, PreviewNoCacheFilter, routes} import jobs.{MessageUsLifecycle, TopicLifecycle} import model.ApplicationIdentity import play.api.ApplicationLoader.Context @@ -32,6 +34,7 @@ import rugby.controllers.RugbyControllers import services.fronts.FrontJsonFapiDraft import services.newsletters.NewsletterSignupLifecycle import services.{ConfigAgentLifecycle, OphanApi, SkimLinksCacheLifeCycle} +import conf.Configuration.aws.mandatoryCredentials trait PreviewLifecycleComponents extends SportServices @@ -92,7 +95,6 @@ trait PreviewControllerComponents lazy val faciaDraftController = wire[FaciaDraftController] lazy val faviconController = wire[FaviconController] lazy val itemController = wire[ItemController] - lazy val oAuthLoginController = wire[OAuthLoginPreviewController] lazy val mostViewedAgent = wire[MostViewedAgent] } @@ -103,6 +105,19 @@ trait AppComponents with OnwardServices with ApplicationsServices { + private lazy val s3Client = + AmazonS3ClientBuilder.standard().withRegion(Regions.EU_WEST_1).withCredentials(mandatoryCredentials).build() + + private lazy val auth = new GuardianAuthWithExemptions( + controllerComponents, + wsClient, + toolsDomainPrefix = "preview", + oauthCallbackPath = routes.GuardianAuthWithExemptions.oauthCallback.path, + s3Client, + system = "preview", + extraDoNotAuthenticatePathPrefixes = healthCheck.healthChecks.map(_.path), + ) + override lazy val capiHttpClient: HttpClient = new CapiHttpClient(wsClient) { override val signer = Some(PreviewSigner()) } @@ -118,7 +133,7 @@ trait AppComponents DCRMetrics.DCRRequestCountMetric, ) - lazy val healthCheck = wire[HealthCheck] + lazy val healthCheck: HealthCheck = wire[HealthCheck] lazy val responsiveViewerController = wire[ResponsiveViewerController] lazy val router: Router = wire[Routes] @@ -127,7 +142,11 @@ trait AppComponents override def lifecycleComponents: List[LifecycleComponent] = standaloneLifecycleComponents :+ wire[CachedHealthCheckLifeCycle] - override lazy val httpFilters: Seq[EssentialFilter] = wire[PreviewFilters].filters + override lazy val httpFilters: Seq[EssentialFilter] = + auth.filter :: new PreviewNoCacheFilter :: new PreviewContentSecurityPolicyFilter :: Filters.common( + frontend.preview.BuildInfo, + ) + override lazy val httpErrorHandler: HttpErrorHandler = wire[PreviewErrorHandler] } diff --git a/preview/app/controllers/OAuthLoginPreviewController.scala b/preview/app/controllers/OAuthLoginPreviewController.scala deleted file mode 100644 index b995d5687596..000000000000 --- a/preview/app/controllers/OAuthLoginPreviewController.scala +++ /dev/null @@ -1,31 +0,0 @@ -package controllers - -import com.gu.googleauth.{GoogleAuthConfig, UserIdentity} -import conf.Configuration -import googleAuth.OAuthLoginController -import model.ApplicationContext -import play.api.http.HttpConfiguration -import play.api.libs.ws.WSClient -import play.api.mvc.{Action, AnyContent, ControllerComponents, Request} - -class OAuthLoginPreviewController( - val wsClient: WSClient, - val httpConfiguration: HttpConfiguration, - val controllerComponents: ControllerComponents, -)(implicit context: ApplicationContext) - extends OAuthLoginController { - - override def login: Action[AnyContent] = - Action { request => - Ok(views.html.previewAuth(context.applicationIdentity.name, "Dev", UserIdentity.fromRequest(request))) - } - override def googleAuthConfig(request: Request[AnyContent]): Option[GoogleAuthConfig] = { - conf - .GoogleAuth( - None, - httpConfiguration, - Configuration.standalone.oauthCredentials, - ) - .config - } -} diff --git a/preview/app/http/PreviewContentSecurityPolicyFilter.scala b/preview/app/http/PreviewContentSecurityPolicyFilter.scala new file mode 100644 index 000000000000..d63a243ff973 --- /dev/null +++ b/preview/app/http/PreviewContentSecurityPolicyFilter.scala @@ -0,0 +1,19 @@ +package http + +import org.apache.pekko.stream.Materializer +import play.api.mvc.{Filter, RequestHeader, Result} + +import scala.concurrent.{ExecutionContext, Future} + +// This should be kept up to date with the prod configured in VCL (fastly-edge-cache repo). +// We don't have fastly or a cache in front of `preview.gutools.co.uk` as it provisioned in CloudFormation in the `platform` repo (private): +// https://github.com/guardian/platform/blob/main/provisioning/cloudformation/frontend.yaml#L824 +class PreviewContentSecurityPolicyFilter(implicit val mat: Materializer, executionContext: ExecutionContext) + extends Filter { + override def apply(nextFilter: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = + nextFilter(request).map( + _.withHeaders( + "Content-Security-Policy" -> "default-src https:; script-src https: 'unsafe-inline' 'unsafe-eval' blob: 'unsafe-inline'; frame-src https: data:; style-src https: 'unsafe-inline'; img-src https: data: blob:; media-src https: data: blob:; font-src https: data:; connect-src https: wss: blob:; child-src https: blob:; object-src 'none'; base-uri https://*.gracenote.com", + ), + ) +} diff --git a/preview/app/http/PreviewFilters.scala b/preview/app/http/PreviewFilters.scala deleted file mode 100644 index 2e888400bfbf..000000000000 --- a/preview/app/http/PreviewFilters.scala +++ /dev/null @@ -1,48 +0,0 @@ -package http - -import org.apache.pekko.stream.Materializer -import CommonAuthFilters.AuthFilterWithExemptions -import controllers.HealthCheck -import model.ApplicationContext -import play.api.http.{HttpConfiguration, HttpFilters} -import play.api.mvc.{Filter, RequestHeader, Result} - -import scala.concurrent.{ExecutionContext, Future} - -class PreviewFilters( - httpConfiguration: HttpConfiguration, - healthCheck: HealthCheck, -)(implicit mat: Materializer, applicationContext: ApplicationContext, executionContext: ExecutionContext) - extends HttpFilters { - - private val exemptionsUrls = healthCheck.healthChecks.map(_.path) - private val filterExemptions = new FilterExemptions(exemptionsUrls: _*) - val previewAuthFilter = new AuthFilterWithExemptions(filterExemptions.loginExemption, filterExemptions.exemptions)( - mat, - applicationContext, - httpConfiguration, - ) - - val filters = previewAuthFilter :: new NoCacheFilter :: new ContentSecurityPolicyFilter :: Filters.common( - frontend.preview.BuildInfo, - ) -} - -// OBVIOUSLY this is only for the preview server -// NOT to be used elsewhere... -class NoCacheFilter(implicit val mat: Materializer, executionContext: ExecutionContext) extends Filter { - override def apply(nextFilter: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = - nextFilter(request).map(_.withHeaders("Cache-Control" -> "no-cache")) -} - -// This should be kept up to date with the prod configured in VCL (fastly-edge-cache repo). -// We don't have fastly or a cache in front of `preview.gutools.co.uk` as it provisioned in CloudFormation in the `platform` repo (private): -// https://github.com/guardian/platform/blob/main/provisioning/cloudformation/frontend.yaml#L824 -class ContentSecurityPolicyFilter(implicit val mat: Materializer, executionContext: ExecutionContext) extends Filter { - override def apply(nextFilter: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = - nextFilter(request).map( - _.withHeaders( - "Content-Security-Policy" -> "default-src https:; script-src https: 'unsafe-inline' 'unsafe-eval' blob: 'unsafe-inline'; frame-src https: data:; style-src https: 'unsafe-inline'; img-src https: data: blob:; media-src https: data: blob:; font-src https: data:; connect-src https: wss: blob:; child-src https: blob:; object-src 'none'; base-uri https://*.gracenote.com", - ), - ) -} diff --git a/preview/app/http/PreviewNoCacheFilter.scala b/preview/app/http/PreviewNoCacheFilter.scala new file mode 100644 index 000000000000..2a1caebf1595 --- /dev/null +++ b/preview/app/http/PreviewNoCacheFilter.scala @@ -0,0 +1,13 @@ +package http + +import org.apache.pekko.stream.Materializer +import play.api.mvc.{Filter, RequestHeader, Result} + +import scala.concurrent.{ExecutionContext, Future} + +// OBVIOUSLY this is only for the preview server +// NOT to be used elsewhere... +class PreviewNoCacheFilter(implicit val mat: Materializer, executionContext: ExecutionContext) extends Filter { + override def apply(nextFilter: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = + nextFilter(request).map(_.withHeaders("Cache-Control" -> "no-cache")) +} diff --git a/preview/app/views/previewAuth.scala.html b/preview/app/views/previewAuth.scala.html deleted file mode 100644 index bdbe2ba9867c..000000000000 --- a/preview/app/views/previewAuth.scala.html +++ /dev/null @@ -1,51 +0,0 @@ -@(title: String, env: String, identity: Option[com.gu.googleauth.UserIdentity] = None) - -@import controllers.routes - - - - - - @title - - - - - - - - -
- -

- @title -

-
- @identity.filter(_.isValid).map { identity => - -
-

You are logged in as @identity.fullName

-
- } -
- - @if(!identity.exists(_.isValid)) { - - } - - - - diff --git a/preview/conf/routes b/preview/conf/routes index 104eab99b01d..7989d5dc2e39 100644 --- a/preview/conf/routes +++ b/preview/conf/routes @@ -10,12 +10,8 @@ GET /assets/internal/*file GET /assets/*path dev.DevAssetsController.at(path) GET /geolocation controllers.FakeGeolocationController.geolocation -#Login # authentication endpoints -GET /login controllers.OAuthLoginPreviewController.login -POST /login controllers.OAuthLoginPreviewController.loginAction -GET /oauth2callback controllers.OAuthLoginPreviewController.oauth2Callback -GET /logout controllers.OAuthLoginPreviewController.logout +GET /oauthCallback http.GuardianAuthWithExemptions.oauthCallback # Crossword GET /crosswords/$crosswordType/:id controllers.CrosswordPageController.crossword(crosswordType: String, id: Int) diff --git a/preview/public/css/style.css b/preview/public/css/style.css index 7b8d92826c98..5fb51c1a4a36 100644 --- a/preview/public/css/style.css +++ b/preview/public/css/style.css @@ -63,43 +63,6 @@ h1 { font-family: Georgia; } -.login-form { - position: absolute; - top: 13px; - left: 160px; - z-index: 1; -} - -.logged-in { - float: right; -} - -.avatar { - float: right; -} - -.logged-in-message { - padding: 0.4em; -} - -.log-in-out { - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 51px; - padding: 15px 20px 0; - font-size: 14px; - float: right; - border-left: 1px solid #cfd8dc; -} - -.log-in-out a { - color: #999999; -} - -.log-in-out a:hover { - color: #333333; -} - .tool { display: inline-block; padding: 0 10px; @@ -135,4 +98,4 @@ h1 { .tools .tool.draft-warning:hover { background: #BBBBBB; -} \ No newline at end of file +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 283f511d2fc7..c6b8231787c1 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -58,9 +58,7 @@ object Dependencies { val macwire = "com.softwaremill.macwire" %% "macros" % "2.5.7" % "provided" val mockito = "org.mockito" % "mockito-all" % "1.10.19" % Test val paClient = "com.gu" %% "pa-client" % "7.0.7" - val playGoogleAuth = "com.gu.play-googleauth" %% "play-v30" % "4.0.0" - val playSecretRotation = "com.gu.play-secret-rotation" %% "play-v30" % "7.1.0" - val playSecretRotationAwsSdk = "com.gu.play-secret-rotation" %% "aws-parameterstore-sdk-v1" % "7.1.0" + val panDomainAuth = "com.gu" %% "pan-domain-auth-play_3-0" % "3.1.0" val quartzScheduler = "org.quartz-scheduler" % "quartz" % "2.3.2" val redisClient = "net.debasishg" %% "redisclient" % "3.42" val rome = "rome" % "rome" % romeVersion