Skip to content

Commit

Permalink
Update to Play 3.0, Scala 2.13 and JDK 17
Browse files Browse the repository at this point in the history
* Bumping pac4j to 6.x and play-pac4j to 12.x
* Play 3.0 includes the akka -> pekko transition
* Explicit scalatest/scalatestplus dependencies
* Docker base image bumped to eclipse-temurin:17-jre-focal
  • Loading branch information
mkr committed Jun 26, 2024
1 parent 5257e1d commit f36fa22
Show file tree
Hide file tree
Showing 49 changed files with 174 additions and 131 deletions.
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# syntax = docker/dockerfile:1.0-experimental
FROM openjdk:11-buster as builder
FROM eclipse-temurin:17-jdk-focal as builder

ARG NODE_VERSION=16

RUN apt-get update \
&& apt-get install -y curl gnupg

RUN echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list \
&& curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | apt-key add \
&& apt-get update \
Expand All @@ -22,7 +25,7 @@ WORKDIR /smui

RUN --mount=target=/root/.ivy2,type=cache sbt "set assembly / test := {}" clean assembly

FROM openjdk:11-jre-slim-buster
FROM eclipse-temurin:17-jre-focal

RUN apt-get update \
&& apt-get install -y --no-install-recommends openssh-client sshpass bash curl git \
Expand Down
26 changes: 14 additions & 12 deletions app/controllers/ApiController.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package controllers

import java.io.{OutputStream, PipedInputStream, PipedOutputStream}
import akka.stream.scaladsl.{Source, StreamConverters}
import akka.util.ByteString
import org.apache.pekko.stream.scaladsl.{Source, StreamConverters}
import org.apache.pekko.util.ByteString

import javax.inject.Inject
import play.api.Logging
Expand All @@ -14,7 +14,6 @@ import java.nio.file.Paths
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime
import scala.concurrent.{ExecutionContext, Future}
import collection.JavaConverters._
import models.FeatureToggleModel.FeatureToggleService
import models._
import models.config.SmuiVersion
Expand All @@ -24,10 +23,11 @@ import models.querqy.QuerqyRulesTxtGenerator
import models.reports.RulesUsageReport
import models.rules.{DeleteRule, FilterRule, RedirectRule, SynonymRule, UpDownRule}
import models.spellings.{CanonicalSpellingId, CanonicalSpellingValidator, CanonicalSpellingWithAlternatives}
import org.pac4j.core.profile.{ProfileManager, UserProfile}
import org.pac4j.play.PlayWebContext
import org.pac4j.core.profile.UserProfile
import org.pac4j.play.context.PlayFrameworkParameters
import org.pac4j.play.scala.{Security, SecurityComponents}
import play.api.libs.Files
import scala.jdk.CollectionConverters.ListHasAsScala
import services.{RulesTxtDeploymentService, RulesTxtImportService, RulesUsageService}


Expand All @@ -48,7 +48,7 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,

case class ApiResult(result: String, message: String, returnId: Option[Id])

implicit val apiResultWrites = Json.writes[ApiResult]
implicit val apiResultWrites: OWrites[ApiResult] = Json.writes[ApiResult]

def getFeatureToggles: Action[AnyContent] = Action {
Ok(Json.toJson(featureToggleService.getJsFrontendToggleList))
Expand Down Expand Up @@ -411,7 +411,7 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,
rules_txt.ref.copyTo(Paths.get(tmp_file_path), replace = true)
// process rules.txt file
val bufferedSource = scala.io.Source.fromFile(tmp_file_path)
val filePayload = bufferedSource.getLines.mkString("\n")
val filePayload = bufferedSource.getLines().mkString("\n")
try {
val importStatistics = rulesTxtImportService.importFromFilePayload(filePayload, SolrIndexId(solrIndexId))
val apiResultMsg = "Import from rules.txt file successful with following statistics:\n" +
Expand Down Expand Up @@ -442,7 +442,7 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,

case class DeploymentInfo(msg: Option[String])

implicit val logDeploymentInfoWrites = Json.writes[DeploymentInfo]
implicit val logDeploymentInfoWrites: OWrites[DeploymentInfo] = Json.writes[DeploymentInfo]

@deprecated("The old style of retrieving a deployment log summary as plain text will be removed", "SMUI version > 3.15.1")
def getLatestDeploymentResultV1(solrIndexId: String, targetSystem: String): Action[AnyContent] = Action.async { request: Request[AnyContent] =>
Expand Down Expand Up @@ -490,7 +490,7 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,

case class DeploymentDetailedInfo(targetSystem: String, formattedDateTime: String, result: Int)

implicit val logDeploymentDetailedInfoWrites = Json.writes[DeploymentDetailedInfo]
implicit val logDeploymentDetailedInfoWrites: OWrites[DeploymentDetailedInfo] = Json.writes[DeploymentDetailedInfo]

def getLatestDeploymentResult(solrIndexId: String): Action[AnyContent] = Action.async { request: Request[AnyContent] =>
Future {
Expand Down Expand Up @@ -543,7 +543,7 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,
val ERROR = Value("ERROR")
}

implicit val smuiVersionInfoWrites = Json.writes[SmuiVersionInfo]
implicit val smuiVersionInfoWrites: OWrites[SmuiVersionInfo] = Json.writes[SmuiVersionInfo]

// TODO consider outsourcing this "business logic" into the (config) model
def getLatestVersionInfo() = Action.async {
Expand Down Expand Up @@ -686,8 +686,10 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,
}

private def getProfiles(request: RequestHeader): List[UserProfile] = {
val webContext = new PlayWebContext(request)
val profileManager = new ProfileManager(webContext, controllerComponents.sessionStore)
val parameters = new PlayFrameworkParameters(request)
val webContext = controllerComponents.config.getWebContextFactory.newContext(parameters)
val sessionStore = controllerComponents.config.getSessionStoreFactory.newSessionStore(parameters)
val profileManager = controllerComponents.config.getProfileManagerFactory.apply(webContext, sessionStore)
profileManager.getProfiles.asScala.toList
}

Expand Down
2 changes: 1 addition & 1 deletion app/models/DatabaseExecutionContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package models

import javax.inject._

import akka.actor.ActorSystem
import org.apache.pekko.actor.ActorSystem
import play.api.libs.concurrent.CustomExecutionContext

/**
Expand Down
2 changes: 1 addition & 1 deletion app/models/SearchManagementRepository.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class SearchManagementRepository @Inject()(dbapi: DBApi, toggleService: FeatureT
def deleteSolrIndex(solrIndexId: String): Int = db.withTransaction { implicit connection =>

val solrIndexIdId = SolrIndexId(solrIndexId)
val inputTags = InputTag.loadAll.filter(_.solrIndexId== Option(solrIndexIdId))
val inputTags = InputTag.loadAll().filter(_.solrIndexId== Option(solrIndexIdId))
if (inputTags.size > 0) {
throw new Exception("Can't delete rules collection that has " + inputTags.size + "tags existing");
}
Expand Down
2 changes: 1 addition & 1 deletion app/models/config/TargetEnvironment.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ package object TargetEnvironment extends Logging {
implicit val jsonFormatTargetEnvironmentGroup: OFormat[TargetEnvironmentGroup] = Json.format[TargetEnvironmentGroup]
implicit val jsonFormatTargetEnvironmentInstance: OFormat[TargetEnvironmentInstance] = Json.format[TargetEnvironmentInstance]

implicit val jsonFormatTargetEnvironmentConfig = new Format[ Seq[TargetEnvironmentInstance] ] {
implicit val jsonFormatTargetEnvironmentConfig: Format[Seq[TargetEnvironmentInstance]] = new Format[Seq[TargetEnvironmentInstance] ] {

def writes(targetEnvironmentConfig: Seq[TargetEnvironmentInstance]): JsValue =
JsArray(
Expand Down
2 changes: 1 addition & 1 deletion app/models/eventhistory/ActivityLog.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ object ActivityLog extends Logging {
private def diffTermStatus(entity: String, beforeTerm: String, beforeStatus: Boolean, afterTerm: String, afterStatus: Boolean, smuiEventType: SmuiEventType.Value): Option[DiffSummary] = {

val termDiff = if (beforeTerm.trim.equals(afterTerm.trim)) None else Some(afterTerm.trim)
val statDiff = if (beforeStatus.equals(afterStatus)) None else Some(afterStatus)
val statDiff = if (beforeStatus == afterStatus) None else Some(afterStatus)

if (termDiff.isDefined || statDiff.isDefined) {

Expand Down
2 changes: 1 addition & 1 deletion app/models/input/TagInputAssociation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ object TagInputAssociation {
map { case tag ~ inputId =>
inputId -> tag
}
}.groupBy(_._1).mapValues(_.map(_._2))
}.groupBy(_._1).view.mapValues(_.map(_._2)).toMap
}

def deleteBySearchInputId(id: SearchInputId)(implicit connection: Connection): Int = {
Expand Down
4 changes: 2 additions & 2 deletions app/models/querqy/QuerqyRulesTxtGenerator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ class QuerqyRulesTxtGenerator @Inject()(searchManagementRepository: SearchManage
}
if (featureToggleService.isRuleTaggingActive) {
val tagsByProperty = searchInput.tags.filter(i => i.exported && i.property.nonEmpty).groupBy(_.property.get)
jsonProperties ++= tagsByProperty.mapValues(tags => Json.toJsFieldJsValueWrapper(tags.map(_.value))).toSeq
jsonProperties ++= tagsByProperty.view.mapValues(tags => Json.toJsFieldJsValueWrapper(tags.map(_.value))).toSeq
}

if (jsonProperties.nonEmpty) {
retSearchInputRulesTxtPartial.append(renderJsonProperties(jsonProperties))
retSearchInputRulesTxtPartial.append(renderJsonProperties(jsonProperties.toSeq))
}

retSearchInputRulesTxtPartial.toString()
Expand Down
3 changes: 2 additions & 1 deletion app/models/rules/Rule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ trait RuleObject[T <: Rule] extends CommonRuleFields {

val sqlParser: RowParser[T]

def updateForSearchInput(searchInputId: SearchInputId, rules: Seq[T])(implicit connection: Connection) {
def updateForSearchInput(searchInputId: SearchInputId, rules: Seq[T])
(implicit connection: Connection): Unit = {
// TODO consider to really determine an update/delete diff to ensure that last_update timestamps only updated for affected rules

SQL"delete from #$TABLE_NAME where #$SEARCH_INPUT_ID = $searchInputId".execute()
Expand Down
2 changes: 1 addition & 1 deletion app/models/rules/SynonymRule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ object SynonymRule extends RuleObjectWithTerm[SynonymRule] {
SQL"select * from #$TABLE_NAME where #$TYPE = #$TYPE_UNDIRECTED AND #$SEARCH_INPUT_ID in ($idGroup)".as((sqlParser ~ get[SearchInputId](SEARCH_INPUT_ID)).*).map { case rule ~ id =>
id -> rule
}
}.groupBy(_._1).mapValues(_.map(_._2))
}.groupBy(_._1).view.mapValues(_.map(_._2)).toMap
}

override def createWithNewIdFrom(rule: SynonymRule): SynonymRule = {
Expand Down
2 changes: 1 addition & 1 deletion app/models/spellings/AlternativeSpelling.scala
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ object AlternativeSpelling {
SQL"select * from #$TABLE_NAME where #$CANONICAL_SPELLING_ID in ($idGroup)".as((sqlParser ~ get[CanonicalSpellingId](CANONICAL_SPELLING_ID)).*).map { case alternativeSpelling ~ id =>
id -> alternativeSpelling
}
}.groupBy(_._1).mapValues(_.map(_._2))
}.groupBy(_._1).view.mapValues(_.map(_._2)).toMap
}
}
1 change: 1 addition & 0 deletions app/models/spellings/CanonicalSpellingValidator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ object CanonicalSpellingValidator {
val allSpellings = allCanonicalSpellings
.flatMap(canonical => canonical.alternativeSpellings.map(alternative => alternative.term.toLowerCase.trim -> canonical.term.toLowerCase.trim))
.groupBy(_._1)
.view
.mapValues(_.map(_._2))
allSpellings.get(canonical).map { canonicalsHavingThatAlternative =>
s"Canonical spelling $canonical is already an alternative spelling of ${canonicalsHavingThatAlternative.mkString(",")}"
Expand Down
16 changes: 10 additions & 6 deletions app/modules/ConfiguredBasicAuthAuthenticator.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package modules

import org.pac4j.core.context.WebContext
import org.pac4j.core.context.session.SessionStore
import org.pac4j.core.credentials.{Credentials, UsernamePasswordCredentials}
import org.pac4j.core.context.CallContext
import org.pac4j.core.credentials.authenticator.Authenticator
import org.pac4j.core.credentials.{Credentials, UsernamePasswordCredentials}
import org.pac4j.core.exception.CredentialsException
import org.pac4j.core.profile.CommonProfile
import org.pac4j.core.util.{CommonHelper, Pac4jConstants}

import java.util.Optional


case class ConfiguredBasicAuthAuthenticator(validUserId: String, validPassword: String) extends Authenticator {

override def validate(credentials: Credentials, context: WebContext, sessionStore: SessionStore): Unit = {
override def validate(callContext: CallContext, credentials: Credentials): Optional[Credentials] = {
import scala.jdk.javaapi.OptionConverters

if (credentials == null) throw new CredentialsException("No credential")
val userCredentials = credentials.asInstanceOf[UsernamePasswordCredentials]
val username = userCredentials.getUsername()
val password = userCredentials.getPassword()
val username = userCredentials.getUsername
val password = userCredentials.getPassword
if (CommonHelper.isBlank(username)) throw new CredentialsException("Username cannot be blank")
if (CommonHelper.isBlank(password)) throw new CredentialsException("Password cannot be blank")
if (CommonHelper.areNotEquals(username.toLowerCase, validUserId.toLowerCase)) throw new CredentialsException("Username : '" + username + "' does not match valid user")
Expand All @@ -24,6 +27,7 @@ case class ConfiguredBasicAuthAuthenticator(validUserId: String, validPassword:
profile.setId(username)
profile.addAttribute(Pac4jConstants.USERNAME, username)
userCredentials.setUserProfile(profile)
OptionConverters.toJava(Some(userCredentials))
}

}
Expand Down
38 changes: 24 additions & 14 deletions app/modules/SecurityModule.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package modules

import com.google.inject.{AbstractModule, Provides}
import org.pac4j.core.client.{Client, Clients}
import org.pac4j.core.client.Clients
import org.pac4j.core.client.direct.AnonymousClient
import org.pac4j.core.config.Config
import org.pac4j.core.context.session.SessionStore
import org.pac4j.core.context.FrameworkParameters
import org.pac4j.core.context.session.{SessionStore, SessionStoreFactory}
import org.pac4j.core.profile.CommonProfile
import org.pac4j.core.profile.factory.ProfileManagerFactory
import org.pac4j.http.client.direct.DirectBasicAuthClient
import org.pac4j.play.context.PlayContextFactory
import org.pac4j.play.http.PlayHttpActionAdapter
import org.pac4j.play.scala.{DefaultSecurityComponents, Pac4jScalaTemplateHelper, SecurityComponents}
import org.pac4j.play.store.{PlayCookieSessionStore, ShiroAesDataEncrypter}
import org.pac4j.play.{CallbackController, LogoutController}
import org.pac4j.play.{CallbackController, LogoutController, PlayWebContext}
import org.pac4j.saml.client.SAML2Client
import org.pac4j.saml.config.SAML2Configuration
import play.api.{Configuration, Environment}
Expand Down Expand Up @@ -42,23 +46,31 @@ class SecurityModule(environment: Environment, configuration: Configuration) ext
}

@Provides
def provideConfig(): Config = {
def provideConfig(sessionStore: SessionStore): Config = {
val maybeConfiguredClientName = configuration.getOptional[String](ConfigKeyAuthClient).filter(_.nonEmpty)
val config: Option[Config] = maybeConfiguredClientName.map {
case "DirectBasicAuthClient" => createConfiguredDirectBasicAuthConfig(s"$ConfigKeyPrefixClientConfig.ConfiguredDirectBasicAuthClient")
case "SAML2Client" => createSaml2Config(s"$ConfigKeyPrefixClientConfig.SAML2Client")
val maybeClients = maybeConfiguredClientName.map {
case "DirectBasicAuthClient" => createConfiguredDirectBasicAuthClient(s"$ConfigKeyPrefixClientConfig.ConfiguredDirectBasicAuthClient")
case "SAML2Client" => createSaml2Client(s"$ConfigKeyPrefixClientConfig.SAML2Client")
case other => throw new RuntimeException(s"Unsupported auth client config value: $other")
}
config.getOrElse(new Config())
val config = new Config()
for (clients <- maybeClients) {
config.setClients(clients)
}
config.setSessionStoreFactory((_: FrameworkParameters) => sessionStore)
config.setHttpActionAdapter(PlayHttpActionAdapter.INSTANCE)
config.setWebContextFactory(PlayContextFactory.INSTANCE)
config.setProfileManagerFactory(ProfileManagerFactory.DEFAULT)
config
}

private def createConfiguredDirectBasicAuthConfig(keyPrefix: String): Config = {
private def createConfiguredDirectBasicAuthClient(keyPrefix: String): Clients = {
val username = configuration.get[String](s"$keyPrefix.username")
val password = configuration.get[String](s"$keyPrefix.password")
new Config(new DirectBasicAuthClient(ConfiguredBasicAuthAuthenticator(username, password)))
new Clients(new DirectBasicAuthClient(ConfiguredBasicAuthAuthenticator(username, password)))
}

private def createSaml2Config(keyPrefix: String): Config = {
private def createSaml2Client(keyPrefix: String): Clients = {
val cfg = new SAML2Configuration(
configuration.get[String](s"$keyPrefix.keystore"),
configuration.get[String](s"$keyPrefix.keystorePassword"),
Expand All @@ -68,10 +80,8 @@ class SecurityModule(environment: Environment, configuration: Configuration) ext
cfg.setServiceProviderEntityId(configuration.get[String](s"$keyPrefix.serviceProviderEntityId"))
cfg.setServiceProviderMetadataPath(configuration.get[String](s"$keyPrefix.serviceProviderMetadataPath"))
cfg.setMaximumAuthenticationLifetime(configuration.get[Long](s"$keyPrefix.maximumAuthenticationLifetime"))
val allClients = Option(new SAML2Client(cfg)).toSeq :+ new AnonymousClient()
// callback URL path as configured in `routes`
val clients = new Clients(s"$baseUrl/callback", allClients:_*)
new Config(clients)
new Clients(s"$baseUrl/callback", new SAML2Client(cfg), new AnonymousClient)
}

}
Expand Down
4 changes: 2 additions & 2 deletions app/services/RulesTxtImportService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class RulesTxtImportService @Inject() (querqyRulesTxtGenerator: QuerqyRulesTxtGe
def tagsFingerprint(input: PreliminarySearchInput): String = {
//Sorting is necessary because play.api.libs.json considers JsArray with same elements but different ordering as not equal. (behaviour tested up to play version 2.8.8)
def sortArrays(json: JsValue): JsValue = json match {
case JsObject(obj) => JsObject(obj.toMap.mapValues(sortArrays(_)).toList)
case JsObject(obj) => JsObject(obj.toMap.view.mapValues(sortArrays).toList)
case JsArray(arr) => JsArray(arr.map(sortArrays).sortBy(_.toString))
case other => other
}
Expand Down Expand Up @@ -257,7 +257,7 @@ class RulesTxtImportService @Inject() (querqyRulesTxtGenerator: QuerqyRulesTxtGe
if(a_synonymRule.term == b_input.term) {
println("^-- Found according synonym on " + b_input.term + " in = " + a_synonymRule)
a_synonymRule.asInstanceOf[PreliminarySynonymRule].synonymType = SynonymRule.TYPE_UNDIRECTED
break
break()
}
}
skip_i += j
Expand Down
2 changes: 1 addition & 1 deletion app/services/RulesUsageService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.apache.commons.csv.CSVFormat
import play.api.{Configuration, Logging}

import javax.inject.Inject
import scala.collection.JavaConverters.iterableAsScalaIterableConverter
import scala.jdk.CollectionConverters.IterableHasAsScala

case class RulesUsage(inputId: SearchInputId,
keywords: String,
Expand Down
Loading

0 comments on commit f36fa22

Please sign in to comment.