From 83e81a276835b0709ae0b10e94aec1c73a7b19a9 Mon Sep 17 00:00:00 2001 From: Roberto Tyley <52038+rtyley@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:22:25 +0000 Subject: [PATCH] Support Scala 3, drop Scala 2.12 & Play 2.8 See: * https://github.com/guardian/pan-domain-authentication/issues/149 - dropping Scala 2.12 * https://github.com/guardian/pan-domain-authentication/issues/161 - supporting Scala 3 ### Need to import `play.api.libs.ws._` for the `BodyWritable` Panda's `OAuth` class posts some url-encoded data (defined as `Map[String, Seq[String]]` in Scala) with `ws.url(dd.token_endpoint).post`, and this needs an implicit instance of `BodyWritable[Map[String, Seq[String]]]` in order to work! For some reason, in Scala 2, the compiler was able to find the correct implicit somewhere, but in Scala 3 we get a compilation error: ``` [error] 81 | }.flatMap { response => [error] | ^ [error] |Cannot find an instance of Map[K, Seq[String]] to WSBody. Define a BodyWritable[Map[K, Seq[String]]] or extend play.api.libs.ws.ahc.DefaultBodyWritables [error] | [error] |where: K is a type variable with constraint >: String [error] | [error] |One of the following imports might fix the problem: [error] | [error] | [error] |One of the following imports might fix the problem: [error] | [error] | import play.api.libs.ws.DefaultBodyWritables.writeableOf_urlEncodedForm [error] | import play.api.libs.ws.WSBodyWritables.writeableOf_urlEncodedForm [error] | import play.api.libs.ws.writeableOf_urlEncodedForm ``` Importing the whole `ws` package fixes the problem: ``` import play.api.libs.ws._ ``` --- .github/workflows/ci.yml | 14 +- build.sbt | 148 ++++++------------ .../app/controllers/AdminController.scala | 12 +- .../com/gu/pandomainauth/service/OAuth.scala | 2 +- .../com/gu/pandomainauth/PanDomain.scala | 2 +- .../gu/pandomainauth/service/CryptoConf.scala | 4 +- project/Dependencies.scala | 39 ++--- project/build.properties | 2 +- project/plugins.sbt | 2 +- 9 files changed, 86 insertions(+), 139 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86f22a3..b0152dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,11 +32,11 @@ jobs: popd - - uses: actions/setup-java@v4 + - uses: guardian/setup-scala@v1 + - name: Build and Test + run: sbt -v clean +test + - name: Test Summary + uses: test-summary/action@v2 with: - java-version: '21' - distribution: 'corretto' - cache: 'sbt' - - - name: Scala Build - run: sbt clean +test + paths: "test-results/**/TEST-*.xml" + if: always() \ No newline at end of file diff --git a/build.sbt b/build.sbt index b434e68..27ab105 100644 --- a/build.sbt +++ b/build.sbt @@ -3,29 +3,30 @@ import sbt.Keys.* import Dependencies.* import sbtrelease.* import ReleaseStateTransformations.* -import xerial.sbt.Sonatype.* import play.sbt.PlayImport.PlayKeys.* import sbtversionpolicy.withsbtrelease.ReleaseVersion -val scala212 = "2.12.20" -val scala213 = "2.13.14" +ThisBuild / scalaVersion := "3.3.4" +ThisBuild / crossScalaVersions := Seq( + scalaVersion.value, + "2.13.15" +) -ThisBuild / scalaVersion := scala213 +val commonSettings = Seq( + organization := "com.gu", + licenses := Seq(License.Apache2), + Test / fork := false, + scalacOptions := Seq( + "-feature", + "-deprecation", + "-release:11" + ), + Test / testOptions += + Tests.Argument(TestFrameworks.ScalaTest, "-u", s"test-results/scala-${scalaVersion.value}", "-o") +) -val commonSettings = - Seq( - crossScalaVersions := List(scala212, scala213), - organization := "com.gu", - Test / fork := false, - scalacOptions ++= Seq( - "-feature", - "-deprecation", - // upgrade warnings to errors except deprecations - "-Wconf:cat=deprecation:ws,any:e", - "-release:11" - ), - licenses := Seq(License.Apache2), - ) +def subproject(path: String): Project = + Project(path, file(path)).settings(commonSettings: _*) lazy val panDomainAuthVerification = subproject("pan-domain-auth-verification") .settings( @@ -34,10 +35,9 @@ lazy val panDomainAuthVerification = subproject("pan-domain-auth-verification") ++ awsDependencies ++ testDependencies ++ loggingDependencies - ++ scalaCollectionCompatDependencies, + :+ scalaCollectionCompat, ) - lazy val panDomainAuthCore = subproject("pan-domain-auth-core") .dependsOn(panDomainAuthVerification) .settings( @@ -46,104 +46,60 @@ lazy val panDomainAuthCore = subproject("pan-domain-auth-core") ++ googleDirectoryApiDependencies ++ cryptoDependencies ++ testDependencies - ++ scalaCollectionCompatDependencies, + :+ scalaCollectionCompat, ) -lazy val panDomainAuthPlay_2_8 = subproject("pan-domain-auth-play_2-8") - .settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-play" / "src") - .settings( - libraryDependencies - ++= playLibs_2_8 - ++ scalaCollectionCompatDependencies, - ).dependsOn(panDomainAuthCore) - -lazy val panDomainAuthPlay_2_9 = subproject("pan-domain-auth-play_2-9") - .settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-play" / "src") - .settings( - crossScalaVersions := Seq(scala213), - libraryDependencies - ++= playLibs_2_9 - ++ scalaCollectionCompatDependencies, - ).dependsOn(panDomainAuthCore) +def playBasedProject(playVersion: PlayVersion, projectPrefix: String, srcFolder: String) = + subproject(s"$projectPrefix${playVersion.projectIdSuffix}").settings( + sourceDirectory := (ThisBuild / baseDirectory).value / srcFolder / "src" + ) -lazy val panDomainAuthPlay_3_0 = subproject("pan-domain-auth-play_3-0") - .settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-play" / "src") - .settings( - crossScalaVersions := Seq(scala213), - libraryDependencies - ++= playLibs_3_0 - ++ scalaCollectionCompatDependencies, +def playSupportFor(playVersion: PlayVersion) = + playBasedProject(playVersion, "pan-domain-auth", "pan-domain-auth-play").settings( + libraryDependencies ++= playVersion.playLibs :+ scalaCollectionCompat ).dependsOn(panDomainAuthCore) -lazy val panDomainAuthHmac_2_8 = subproject("panda-hmac-play_2-8") - .settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-hmac" / "src") - .settings( - libraryDependencies ++= hmacLibs ++ playLibs_2_8 ++ testDependencies, - ).dependsOn(panDomainAuthPlay_2_8) +def hmacPlayProject(playVersion: PlayVersion, playSupportProject: Project) = + playBasedProject(playVersion, "panda-hmac", "pan-domain-auth-hmac").settings( + libraryDependencies ++= hmacHeaders +: testDependencies + ).dependsOn(playSupportProject) -lazy val panDomainAuthHmac_2_9 = subproject("panda-hmac-play_2-9") - .settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-hmac" / "src") - .settings( - crossScalaVersions := Seq(scala213), - libraryDependencies ++= hmacLibs ++ playLibs_2_9 ++ testDependencies, - ).dependsOn(panDomainAuthPlay_2_9) +lazy val panDomainAuthPlay_2_9 = playSupportFor(PlayVersion.V29) +lazy val panDomainAuthHmac_2_9 = hmacPlayProject(PlayVersion.V29, panDomainAuthPlay_2_9) -lazy val panDomainAuthHmac_3_0 = subproject("panda-hmac-play_3-0") - .settings(sourceDirectory := (ThisBuild / baseDirectory).value / "pan-domain-auth-hmac" / "src") - .settings( - crossScalaVersions := Seq(scala213), - libraryDependencies ++= hmacLibs ++ playLibs_3_0 ++ testDependencies, - ).dependsOn(panDomainAuthPlay_3_0) +lazy val panDomainAuthPlay_3_0 = playSupportFor(PlayVersion.V30) +lazy val panDomainAuthHmac_3_0 = hmacPlayProject(PlayVersion.V30, panDomainAuthPlay_3_0) lazy val exampleApp = subproject("pan-domain-auth-example") .enablePlugins(PlayScala) - .settings(libraryDependencies ++= (awsDependencies :+ ws)) - .dependsOn(panDomainAuthPlay_2_9) + .dependsOn(panDomainAuthPlay_3_0) .settings( - crossScalaVersions := Seq(scala213), + libraryDependencies ++= awsDependencies :+ ws, publish / skip := true, playDefaultPort := 9500 ) -lazy val sonatypeReleaseSettings = { - sonatypeSettings ++ Seq( - // sbt and sbt-release implement cross-building support differently. sbt does it better - // (it supports each subproject having different crossScalaVersions), so disable sbt-release's - // implementation, and do the publish step with a `+`, - // ie. (`releaseStepCommandAndRemaining("+publishSigned")`) - // See https://www.scala-sbt.org/1.x/docs/Cross-Build.html#Note+about+sbt-release - // Never run with "release cross" or "+release"! Odd things start happening - releaseCrossBuild := false, - releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value, - releaseProcess := Seq[ReleaseStep]( - checkSnapshotDependencies, - inquireVersions, - runClean, - runTest, - setReleaseVersion, - commitReleaseVersion, - tagRelease, - setNextVersion, - commitNextVersion - ) - ) -} - lazy val root = Project("pan-domain-auth-root", file(".")).aggregate( panDomainAuthVerification, panDomainAuthCore, - panDomainAuthPlay_2_8, panDomainAuthPlay_2_9, panDomainAuthPlay_3_0, - panDomainAuthHmac_2_8, panDomainAuthHmac_2_9, panDomainAuthHmac_3_0, exampleApp -).settings(sonatypeReleaseSettings) - .settings( - organization := "com.gu", +).settings( publish / skip := true, + releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value, + releaseCrossBuild := true, // true if you cross-build the project for multiple Scala versions + releaseProcess := Seq[ReleaseStep]( + checkSnapshotDependencies, + inquireVersions, + runClean, + runTest, + setReleaseVersion, + commitReleaseVersion, + tagRelease, + setNextVersion, + commitNextVersion + ) ) - -def subproject(path: String): Project = - Project(path, file(path)).settings(commonSettings: _*) diff --git a/pan-domain-auth-example/app/controllers/AdminController.scala b/pan-domain-auth-example/app/controllers/AdminController.scala index ea8d2b4..b76f897 100644 --- a/pan-domain-auth-example/app/controllers/AdminController.scala +++ b/pan-domain-auth-example/app/controllers/AdminController.scala @@ -2,7 +2,7 @@ package controllers import com.gu.pandomainauth.PanDomainAuthSettingsRefresher import play.api.Configuration -import play.api.mvc.{AbstractController, ControllerComponents} +import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} import play.api.libs.ws.WSClient @@ -14,32 +14,32 @@ class AdminController( ) extends AbstractController(controllerComponents) with ExampleAuthActions { // No authentication - def index = Action{ + def index: Action[AnyContent] = Action { Ok("hello") } // This is a normal user-interactive request that will redirect to the OAuth provider // to re-negotiate a login on expiry. - def showUser = AuthAction { req => + def showUser: Action[AnyContent] = AuthAction { req => // The user information is available as a field on the request Ok(req.user.toString) } // This is a request that is issued from JS. If the user has expired it will return an // error code that can be handled by the front-end webapp. - def showUserApi = APIAuthAction { req => + def showUserApi: Action[AnyContent] = APIAuthAction { req => Ok(req.user.toString) } // Required to allow the provider to redirect back to us so we can issue the new cookie // This route must be added to the provider whitelist - def oauthCallback = Action.async { implicit request => + def oauthCallback: Action[AnyContent] = Action.async { implicit request => processOAuthCallback() } // Note: this is potentially confusing depending on your use-case as currently only the // panda cookie is removed and the user is not logged out of the OAuth provider - def logout = Action { implicit request => + def logout: Action[AnyContent] = Action { implicit request => processLogout(request) } } diff --git a/pan-domain-auth-play/src/main/scala/com/gu/pandomainauth/service/OAuth.scala b/pan-domain-auth-play/src/main/scala/com/gu/pandomainauth/service/OAuth.scala index 320c4e8..e934b7e 100644 --- a/pan-domain-auth-play/src/main/scala/com/gu/pandomainauth/service/OAuth.scala +++ b/pan-domain-auth-play/src/main/scala/com/gu/pandomainauth/service/OAuth.scala @@ -2,7 +2,7 @@ package com.gu.pandomainauth.service import com.gu.pandomainauth.model.{AuthenticatedUser, OAuthSettings, User} import play.api.libs.json.JsValue -import play.api.libs.ws.{WSClient, WSResponse} +import play.api.libs.ws._ import play.api.mvc.Results.Redirect import play.api.mvc.{RequestHeader, Result} diff --git a/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/PanDomain.scala b/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/PanDomain.scala index 68ca4a0..1dcfc5b 100644 --- a/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/PanDomain.scala +++ b/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/PanDomain.scala @@ -11,7 +11,7 @@ object PanDomain { */ def authStatus(cookieData: String, verification: Verification, validateUser: AuthenticatedUser => Boolean, apiGracePeriod: Long, system: String, cacheValidation: Boolean, forceExpiry: Boolean): AuthenticationStatus = { - CookieUtils.parseCookieData(cookieData, verification).fold(InvalidCookie, { authedUser => + CookieUtils.parseCookieData(cookieData, verification).fold(InvalidCookie(_), { authedUser => checkStatus(authedUser, validateUser, apiGracePeriod, system, cacheValidation, forceExpiry) }) } diff --git a/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/service/CryptoConf.scala b/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/service/CryptoConf.scala index 7e2c3e2..fa5098d 100644 --- a/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/service/CryptoConf.scala +++ b/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/service/CryptoConf.scala @@ -38,8 +38,8 @@ object CryptoConf { case class SettingsReader(settingMap: Map[String,String]) { def setting(key: String): SettingsResult[String] = settingMap.get(key).toRight(MissingSetting(key)) - def signingAndVerificationConf: SettingsResult[SigningAndVerification] = makeConfWith(activeKeyPair)(SigningAndVerification) - def verificationConf: SettingsResult[Verification] = makeConfWith(activePublicKey)(OnlyVerification) + def signingAndVerificationConf: SettingsResult[SigningAndVerification] = makeConfWith(activeKeyPair)(SigningAndVerification(_, _)) + def verificationConf: SettingsResult[Verification] = makeConfWith(activePublicKey)(OnlyVerification(_, _)) val activePublicKey: SettingsResult[PublicKey] = setting("publicKey").flatMap(publicKeyFor) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d3ca4a4..92f3d1a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,33 +4,24 @@ object Dependencies { val awsDependencies = Seq("com.amazonaws" % "aws-java-sdk-s3" % "1.12.772") - val playLibs_2_8 = { - val version = "2.8.19" - Seq( - "com.typesafe.play" %% "play" % version % "provided", - "com.typesafe.play" %% "play-ws" % version % "provided" - ) + case class PlayVersion( + majorVersion: Int, + minorVersion: Int, + groupId: String, + exactPlayVersion: String + ) { + val projectIdSuffix = s"-play_$majorVersion-$minorVersion" + + val playLibs: Seq[ModuleID] = + Seq("play", "play-ws").map(artifact => groupId %% artifact % exactPlayVersion) } - val playLibs_2_9 = { - val version = "2.9.0" - Seq( - "com.typesafe.play" %% "play" % version % "provided", - "com.typesafe.play" %% "play-ws" % version % "provided" - ) + object PlayVersion { + val V29 = PlayVersion(2, 9, "com.typesafe.play", "2.9.2") + val V30 = PlayVersion(3, 0, "org.playframework", "3.0.5") } - val playLibs_3_0 = { - val version = "3.0.0" - Seq( - "org.playframework" %% "play" % version % "provided", - "org.playframework" %% "play-ws" % version % "provided" - ) - } - - val hmacLibs = Seq( - "com.gu" %% "hmac-headers" % "2.0.0" - ) + val hmacHeaders = "com.gu" %% "hmac-headers" % "2.0.1" val googleDirectoryApiDependencies = Seq( "com.google.apis" % "google-api-services-admin-directory" % "directory_v1-rev20240903-2.0.0", @@ -49,5 +40,5 @@ object Dependencies { // provide compatibility between scala 2.12 and 2.13 // see https://github.com/scala/scala-collection-compat/issues/208 - val scalaCollectionCompatDependencies = Seq("org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0") + val scalaCollectionCompat = "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0" } diff --git a/project/build.properties b/project/build.properties index 04267b1..db1723b 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.5 diff --git a/project/plugins.sbt b/project/plugins.sbt index 3bd9163..2fd023b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // Use the Play sbt plugin for Play projects -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.5") +addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.5") addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0")