diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index 37f509d..5498b11 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -1,30 +1,26 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.Request -import org.pac4j.http4s.Http4sWebContext +import org.pac4j.core.profile.CommonProfile -type AppTask = RIO[ZEnv, *] +type AppEnv = ZEnv +type AppTask = RIO[AppEnv, *] +type AppAuth = List[CommonProfile] object Main extends ZIOAppDefault: - // TODO: move inside HttpApplication (using ZIO.runtime) - private val contextBuilder = - (req: Request[AppTask], conf: org.pac4j.core.config.Config) => - new Http4sWebContext[AppTask]( - req, - conf.getSessionStore, - runtime.unsafeRun(_) - ) + lazy val runtimeLayer = ZLayer.fromZIO(ZIO.runtime[AppEnv]) + lazy val securityLayer = + security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer + lazy val appLayer = + AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + lazy val serverLayer = + blaze.BlazeServerConfig.fromEnv >+> appLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] - .provideCustom( - HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer - ) + .provideCustom(serverLayer) _ <- server.serve() } yield () diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index 37f509d..5498b11 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -1,30 +1,26 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.Request -import org.pac4j.http4s.Http4sWebContext +import org.pac4j.core.profile.CommonProfile -type AppTask = RIO[ZEnv, *] +type AppEnv = ZEnv +type AppTask = RIO[AppEnv, *] +type AppAuth = List[CommonProfile] object Main extends ZIOAppDefault: - // TODO: move inside HttpApplication (using ZIO.runtime) - private val contextBuilder = - (req: Request[AppTask], conf: org.pac4j.core.config.Config) => - new Http4sWebContext[AppTask]( - req, - conf.getSessionStore, - runtime.unsafeRun(_) - ) + lazy val runtimeLayer = ZLayer.fromZIO(ZIO.runtime[AppEnv]) + lazy val securityLayer = + security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer + lazy val appLayer = + AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + lazy val serverLayer = + blaze.BlazeServerConfig.fromEnv >+> appLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] - .provideCustom( - HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer - ) + .provideCustom(serverLayer) _ <- server.serve() } yield () diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala new file mode 100644 index 0000000..cf8a89d --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.HttpRoutes + +object BlazeHttpServer { + val layer: URLayer[BlazeServerConfig & HttpApplication, HttpServer] = + val routesLayer = ZLayer + .environment[HttpApplication] + .flatMap(a => ZLayer.fromZIO(a.get.routes())) + val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] + (ZLayer.environment[BlazeServerConfig] ++ routesLayer) >>> blazeLayer +} + +import BlazeHttpServer.* + +case class BlazeHttpServer( + config: BlazeServerConfig, + httpApp: HttpRoutes[AppTask] +) extends HttpServer: + override def serve(): UIO[ExitCode] = + BlazeServerBuilder[AppTask] + .bindHttp(config.port, config.host) + .withHttpApp(httpApp.orNotFound) + .serve + .compile + .drain + .fold(_ => ExitCode.failure, _ => ExitCode.success) + .provideEnvironment(ZEnvironment.default) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index 37f509d..5498b11 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -1,30 +1,26 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.Request -import org.pac4j.http4s.Http4sWebContext +import org.pac4j.core.profile.CommonProfile -type AppTask = RIO[ZEnv, *] +type AppEnv = ZEnv +type AppTask = RIO[AppEnv, *] +type AppAuth = List[CommonProfile] object Main extends ZIOAppDefault: - // TODO: move inside HttpApplication (using ZIO.runtime) - private val contextBuilder = - (req: Request[AppTask], conf: org.pac4j.core.config.Config) => - new Http4sWebContext[AppTask]( - req, - conf.getSessionStore, - runtime.unsafeRun(_) - ) + lazy val runtimeLayer = ZLayer.fromZIO(ZIO.runtime[AppEnv]) + lazy val securityLayer = + security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer + lazy val appLayer = + AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + lazy val serverLayer = + blaze.BlazeServerConfig.fromEnv >+> appLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] - .provideCustom( - HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer - ) + .provideCustom(serverLayer) _ <- server.serve() } yield () diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala new file mode 100644 index 0000000..cf8a89d --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.HttpRoutes + +object BlazeHttpServer { + val layer: URLayer[BlazeServerConfig & HttpApplication, HttpServer] = + val routesLayer = ZLayer + .environment[HttpApplication] + .flatMap(a => ZLayer.fromZIO(a.get.routes())) + val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] + (ZLayer.environment[BlazeServerConfig] ++ routesLayer) >>> blazeLayer +} + +import BlazeHttpServer.* + +case class BlazeHttpServer( + config: BlazeServerConfig, + httpApp: HttpRoutes[AppTask] +) extends HttpServer: + override def serve(): UIO[ExitCode] = + BlazeServerBuilder[AppTask] + .bindHttp(config.port, config.host) + .withHttpApp(httpApp.orNotFound) + .serve + .compile + .drain + .fold(_ => ExitCode.failure, _ => ExitCode.success) + .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala new file mode 100644 index 0000000..01232b8 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.config.* + +case class BlazeServerConfig(host: String, port: Int) + +object BlazeServerConfig: + val configDesc: ConfigDescriptor[BlazeServerConfig] = + import ConfigDescriptor.* + nested("BLAZE")( + string("HOST").default("localhost") zip int("PORT").default(8080) + ).to[BlazeServerConfig] + + val fromEnv = ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index 37f509d..5498b11 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -1,30 +1,26 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.Request -import org.pac4j.http4s.Http4sWebContext +import org.pac4j.core.profile.CommonProfile -type AppTask = RIO[ZEnv, *] +type AppEnv = ZEnv +type AppTask = RIO[AppEnv, *] +type AppAuth = List[CommonProfile] object Main extends ZIOAppDefault: - // TODO: move inside HttpApplication (using ZIO.runtime) - private val contextBuilder = - (req: Request[AppTask], conf: org.pac4j.core.config.Config) => - new Http4sWebContext[AppTask]( - req, - conf.getSessionStore, - runtime.unsafeRun(_) - ) + lazy val runtimeLayer = ZLayer.fromZIO(ZIO.runtime[AppEnv]) + lazy val securityLayer = + security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer + lazy val appLayer = + AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + lazy val serverLayer = + blaze.BlazeServerConfig.fromEnv >+> appLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] - .provideCustom( - HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer - ) + .provideCustom(serverLayer) _ <- server.serve() } yield () diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala new file mode 100644 index 0000000..cf8a89d --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.HttpRoutes + +object BlazeHttpServer { + val layer: URLayer[BlazeServerConfig & HttpApplication, HttpServer] = + val routesLayer = ZLayer + .environment[HttpApplication] + .flatMap(a => ZLayer.fromZIO(a.get.routes())) + val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] + (ZLayer.environment[BlazeServerConfig] ++ routesLayer) >>> blazeLayer +} + +import BlazeHttpServer.* + +case class BlazeHttpServer( + config: BlazeServerConfig, + httpApp: HttpRoutes[AppTask] +) extends HttpServer: + override def serve(): UIO[ExitCode] = + BlazeServerBuilder[AppTask] + .bindHttp(config.port, config.host) + .withHttpApp(httpApp.orNotFound) + .serve + .compile + .drain + .fold(_ => ExitCode.failure, _ => ExitCode.success) + .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala new file mode 100644 index 0000000..01232b8 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.config.* + +case class BlazeServerConfig(host: String, port: Int) + +object BlazeServerConfig: + val configDesc: ConfigDescriptor[BlazeServerConfig] = + import ConfigDescriptor.* + nested("BLAZE")( + string("HOST").default("localhost") zip int("PORT").default(8080) + ).to[BlazeServerConfig] + + val fromEnv = ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala new file mode 100644 index 0000000..b2e4e62 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala @@ -0,0 +1,122 @@ +package mdr.pdb.server +package security + +import java.util.Optional +import org.http4s.HttpRoutes +import org.http4s.Request +import org.http4s.ResponseCookie +import org.http4s.server.AuthMiddleware +import org.pac4j.core.authorization.generator.AuthorizationGenerator +import org.pac4j.core.client.Clients +import org.pac4j.core.config.Config +import org.pac4j.core.context.WebContext +import org.pac4j.core.context.session.SessionStore +import org.pac4j.core.profile.CommonProfile +import org.pac4j.core.profile.UserProfile +import org.pac4j.http4s.* +import org.pac4j.oidc.client.OidcClient +import org.pac4j.oidc.config.OidcConfiguration +import scala.concurrent.duration.{*, given} +import zio.* +import zio.config.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} + +object Pac4jHttpSecurity: + val layer: URLayer[Pac4jSecurityConfig & Runtime[AppEnv], HttpSecurity] = + (Pac4jHttpSecurity(_, _)).toLayer[HttpSecurity] + +class Pac4jHttpSecurity( + config: Pac4jSecurityConfig, + runtime: Runtime[AppEnv] +) extends HttpSecurity + with CustomDsl: + private val contextBuilder = + (req: Request[AppTask], conf: org.pac4j.core.config.Config) => + new Http4sWebContext[AppTask]( + req, + conf.getSessionStore, + runtime.unsafeRun(_) + ) + + def oidcClient(): OidcClient = { + val oidcConfiguration = new OidcConfiguration() + oidcConfiguration.setClientId(config.clientId) + oidcConfiguration.setSecret(config.clientSecret) + oidcConfiguration.setDiscoveryURI(config.discoveryURI) + oidcConfiguration.setUseNonce(true) + // oidcConfiguration.addCustomParam("prompt", "consent") + val oidcClient = new OidcClient(oidcConfiguration) + + val authorizationGenerator = new AuthorizationGenerator { + override def generate( + context: WebContext, + sessionStore: SessionStore, + profile: UserProfile + ): Optional[UserProfile] = { + // profile.addRole("ROLE_ADMIN") + Optional.of(profile) + } + } + oidcClient.setAuthorizationGenerator(authorizationGenerator) + oidcClient + } + + val pac4jConfig = + val clients = + Clients( + s"${config.urlBase}/${config.callbackBase}/callback", + oidcClient() + ) + val conf = org.pac4j.core.config.Config(clients) + conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) + conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) + conf + + private val sessionConfig = SessionConfig( + cookieName = "session", + mkCookie = ResponseCookie(_, _, path = Some("/")), + secret = "This is a secret", + maxAge = 5.minutes + ) + + val callbackService = + CallbackService[AppTask](pac4jConfig, contextBuilder) + + val localLogoutService = LogoutService[AppTask]( + pac4jConfig, + contextBuilder, + config.logoutUrl, + destroySession = true + ) + val centralLogoutService = LogoutService[AppTask]( + pac4jConfig, + contextBuilder, + defaultUrl = config.logoutUrl, + logoutUrlPattern = Some(s"${config.logoutUrl}.*"), + localLogout = false, + destroySession = true, + centralLogout = true + ) + + val sessionManagement = Session.sessionManagement[AppTask](sessionConfig) + val securityFilter = SecurityFilterMiddleware + .securityFilter[AppTask](pac4jConfig, contextBuilder) + + val routes: HttpRoutes[AppTask] = + HttpRoutes.of { + case req @ GET -> Root / "callback" => + callbackService.callback(req) + case req @ POST -> Root / "callback" => + callbackService.callback(req) + case req @ GET -> Root / "logout" => + localLogoutService.logout(req) + case req @ GET -> Root / "centralLogout" => + centralLogoutService.logout(req) + } + + override val route: (String, HttpRoutes[AppTask]) = + s"/${config.callbackBase}" -> sessionManagement(routes) + + override def secure: AuthMiddleware[AppTask, List[CommonProfile]] = + sessionManagement.compose(securityFilter) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index 37f509d..5498b11 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -1,30 +1,26 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.Request -import org.pac4j.http4s.Http4sWebContext +import org.pac4j.core.profile.CommonProfile -type AppTask = RIO[ZEnv, *] +type AppEnv = ZEnv +type AppTask = RIO[AppEnv, *] +type AppAuth = List[CommonProfile] object Main extends ZIOAppDefault: - // TODO: move inside HttpApplication (using ZIO.runtime) - private val contextBuilder = - (req: Request[AppTask], conf: org.pac4j.core.config.Config) => - new Http4sWebContext[AppTask]( - req, - conf.getSessionStore, - runtime.unsafeRun(_) - ) + lazy val runtimeLayer = ZLayer.fromZIO(ZIO.runtime[AppEnv]) + lazy val securityLayer = + security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer + lazy val appLayer = + AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + lazy val serverLayer = + blaze.BlazeServerConfig.fromEnv >+> appLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] - .provideCustom( - HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer - ) + .provideCustom(serverLayer) _ <- server.serve() } yield () diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala new file mode 100644 index 0000000..cf8a89d --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.HttpRoutes + +object BlazeHttpServer { + val layer: URLayer[BlazeServerConfig & HttpApplication, HttpServer] = + val routesLayer = ZLayer + .environment[HttpApplication] + .flatMap(a => ZLayer.fromZIO(a.get.routes())) + val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] + (ZLayer.environment[BlazeServerConfig] ++ routesLayer) >>> blazeLayer +} + +import BlazeHttpServer.* + +case class BlazeHttpServer( + config: BlazeServerConfig, + httpApp: HttpRoutes[AppTask] +) extends HttpServer: + override def serve(): UIO[ExitCode] = + BlazeServerBuilder[AppTask] + .bindHttp(config.port, config.host) + .withHttpApp(httpApp.orNotFound) + .serve + .compile + .drain + .fold(_ => ExitCode.failure, _ => ExitCode.success) + .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala new file mode 100644 index 0000000..01232b8 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.config.* + +case class BlazeServerConfig(host: String, port: Int) + +object BlazeServerConfig: + val configDesc: ConfigDescriptor[BlazeServerConfig] = + import ConfigDescriptor.* + nested("BLAZE")( + string("HOST").default("localhost") zip int("PORT").default(8080) + ).to[BlazeServerConfig] + + val fromEnv = ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala new file mode 100644 index 0000000..b2e4e62 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala @@ -0,0 +1,122 @@ +package mdr.pdb.server +package security + +import java.util.Optional +import org.http4s.HttpRoutes +import org.http4s.Request +import org.http4s.ResponseCookie +import org.http4s.server.AuthMiddleware +import org.pac4j.core.authorization.generator.AuthorizationGenerator +import org.pac4j.core.client.Clients +import org.pac4j.core.config.Config +import org.pac4j.core.context.WebContext +import org.pac4j.core.context.session.SessionStore +import org.pac4j.core.profile.CommonProfile +import org.pac4j.core.profile.UserProfile +import org.pac4j.http4s.* +import org.pac4j.oidc.client.OidcClient +import org.pac4j.oidc.config.OidcConfiguration +import scala.concurrent.duration.{*, given} +import zio.* +import zio.config.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} + +object Pac4jHttpSecurity: + val layer: URLayer[Pac4jSecurityConfig & Runtime[AppEnv], HttpSecurity] = + (Pac4jHttpSecurity(_, _)).toLayer[HttpSecurity] + +class Pac4jHttpSecurity( + config: Pac4jSecurityConfig, + runtime: Runtime[AppEnv] +) extends HttpSecurity + with CustomDsl: + private val contextBuilder = + (req: Request[AppTask], conf: org.pac4j.core.config.Config) => + new Http4sWebContext[AppTask]( + req, + conf.getSessionStore, + runtime.unsafeRun(_) + ) + + def oidcClient(): OidcClient = { + val oidcConfiguration = new OidcConfiguration() + oidcConfiguration.setClientId(config.clientId) + oidcConfiguration.setSecret(config.clientSecret) + oidcConfiguration.setDiscoveryURI(config.discoveryURI) + oidcConfiguration.setUseNonce(true) + // oidcConfiguration.addCustomParam("prompt", "consent") + val oidcClient = new OidcClient(oidcConfiguration) + + val authorizationGenerator = new AuthorizationGenerator { + override def generate( + context: WebContext, + sessionStore: SessionStore, + profile: UserProfile + ): Optional[UserProfile] = { + // profile.addRole("ROLE_ADMIN") + Optional.of(profile) + } + } + oidcClient.setAuthorizationGenerator(authorizationGenerator) + oidcClient + } + + val pac4jConfig = + val clients = + Clients( + s"${config.urlBase}/${config.callbackBase}/callback", + oidcClient() + ) + val conf = org.pac4j.core.config.Config(clients) + conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) + conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) + conf + + private val sessionConfig = SessionConfig( + cookieName = "session", + mkCookie = ResponseCookie(_, _, path = Some("/")), + secret = "This is a secret", + maxAge = 5.minutes + ) + + val callbackService = + CallbackService[AppTask](pac4jConfig, contextBuilder) + + val localLogoutService = LogoutService[AppTask]( + pac4jConfig, + contextBuilder, + config.logoutUrl, + destroySession = true + ) + val centralLogoutService = LogoutService[AppTask]( + pac4jConfig, + contextBuilder, + defaultUrl = config.logoutUrl, + logoutUrlPattern = Some(s"${config.logoutUrl}.*"), + localLogout = false, + destroySession = true, + centralLogout = true + ) + + val sessionManagement = Session.sessionManagement[AppTask](sessionConfig) + val securityFilter = SecurityFilterMiddleware + .securityFilter[AppTask](pac4jConfig, contextBuilder) + + val routes: HttpRoutes[AppTask] = + HttpRoutes.of { + case req @ GET -> Root / "callback" => + callbackService.callback(req) + case req @ POST -> Root / "callback" => + callbackService.callback(req) + case req @ GET -> Root / "logout" => + localLogoutService.logout(req) + case req @ GET -> Root / "centralLogout" => + centralLogoutService.logout(req) + } + + override val route: (String, HttpRoutes[AppTask]) = + s"/${config.callbackBase}" -> sessionManagement(routes) + + override def secure: AuthMiddleware[AppTask, List[CommonProfile]] = + sessionManagement.compose(securityFilter) diff --git a/server/src/main/scala/mdr/pdb/server/security/Pac4jSecurityConfig.scala b/server/src/main/scala/mdr/pdb/server/security/Pac4jSecurityConfig.scala new file mode 100644 index 0000000..72c1c7e --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/security/Pac4jSecurityConfig.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package security + +import zio.* +import zio.config.* + +case class Pac4jSecurityConfig( + urlBase: String, + callbackBase: String, + logoutUrl: Option[String], + clientId: String, + clientSecret: String, + discoveryURI: String +) + +object Pac4jSecurityConfig: + val configDesc: ConfigDescriptor[Pac4jSecurityConfig] = + import ConfigDescriptor.* + nested("SECURITY")( + string("URLBASE") zip string("CALLBACKBASE") zip string( + "LOGOUTURL" + ).optional zip string("CLIENTID") zip string("CLIENTSECRET") + zip string( + "DISCOVERYURI" + ) + ).to[Pac4jSecurityConfig] + + val fromEnv: ZLayer[System, ReadError[String], Pac4jSecurityConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..7bc4c8d --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ + -mem 2048 diff --git a/app/scala-version.js b/app/scala-version.js index fa5aadf..868d6c1 100644 --- a/app/scala-version.js +++ b/app/scala-version.js @@ -1,2 +1,2 @@ // TODO: generate from the build info -module.exports = "3.1.0" +module.exports = "3.1.1" diff --git a/build.sbt b/build.sbt index 247d3d5..2afc879 100644 --- a/build.sbt +++ b/build.sbt @@ -60,6 +60,7 @@ IWDeps.zioInteropCats, IWDeps.tapirCore, IWDeps.tapirZIO, + IWDeps.tapirZIOJson, IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.logbackClassic, @@ -77,7 +78,16 @@ "BLAZE_PORT" -> "8080", "APP_PATH" -> "/opt/docker/vite" ), - reStart / envVars := Map("APP_PATH" -> "../app/target/vite") + reStart / envVars := Map( + "APP_PATH" -> "../app/target/vite", + "SECURITY_URLBASE" -> "http://localhost:8080", + "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", + "SECURITY_CALLBACKBASE" -> "mdr/pdb/auth/", + "SECURITY_LOGOUTURL" -> "https://tc163.cmi.cz/mdr/app", + "SECURITY_CLIENTID" -> "mdrpdbtest", + "SECURITY_CLIENTSECRET" -> "aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02" + ) + // Revolver.enableDebugging(port = 5005, suspend = true) ) lazy val root = (project in file(".")) diff --git a/project/build.properties b/project/build.properties index 3161d21..c8fcab5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 diff --git a/server/src/main/scala/mdr/pdb/server/AppConfig.scala b/server/src/main/scala/mdr/pdb/server/AppConfig.scala new file mode 100644 index 0000000..26a51db --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/AppConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server + +import zio.* +import zio.config.* + +case class AppConfig(appPath: String, urlBase: String) + +object AppConfig: + val configDesc: ConfigDescriptor[AppConfig] = + import ConfigDescriptor.* + nested("APP")( + string("PATH") zip string("BASE").default("http://localhost:8080") + ).to[AppConfig] + + val fromEnv: ZLayer[System, ReadError[String], AppConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/CustomDsl.scala b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala new file mode 100644 index 0000000..81f20b3 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomDsl.scala @@ -0,0 +1,5 @@ +package mdr.pdb.server + +import org.http4s.dsl.Http4sDsl + +trait CustomDsl extends Http4sDsl[AppTask] diff --git a/server/src/main/scala/mdr/pdb/server/CustomTapir.scala b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala new file mode 100644 index 0000000..bf8e8dd --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/CustomTapir.scala @@ -0,0 +1,15 @@ +package mdr.pdb.server + +import sttp.tapir.Tapir +import sttp.tapir.TapirAliases +import sttp.tapir.json.zio.TapirJsonZio +import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter +import zio.* +import sttp.tapir.ztapir.ZTapir + +object CustomTapir + extends Tapir + with ZTapir + with ZHttp4sServerInterpreter[AppEnv] + with TapirJsonZio + with TapirAliases diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 6b00719..5889d48 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -12,159 +12,30 @@ import org.http4s.server.Router import org.http4s.syntax.all.{*, given} -import sttp.tapir.* -import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter - import org.pac4j.http4s.* - -import org.pac4j.core.authorization.generator.AuthorizationGenerator -import org.pac4j.core.client.Clients -import org.pac4j.core.config.Config -import org.pac4j.core.context.WebContext -import org.pac4j.core.context.session.SessionStore import org.pac4j.core.profile.CommonProfile -import org.pac4j.core.profile.UserProfile -import org.pac4j.oidc.client.OidcClient -import org.pac4j.oidc.config.OidcConfiguration - -import scala.concurrent.duration.{*, given} -import java.util.Optional trait HttpApplication { def routes(): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - import zio.config.* - - case class AppConfig(appPath: String, urlBase: String) - - val appConfigDesc: ConfigDescriptor[AppConfig] = - import ConfigDescriptor.* - nested("APP")( - string("PATH") zip string("BASE").default("http://localhost:8080") - ).to[AppConfig] - - def layer( - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] - ): RLayer[System, HttpApplication] = - val configLayer = ZConfig.fromSystemEnv( - appConfigDesc, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val appLayer = - (HttpApplicationLive(_, contextBuilder)).toLayer[HttpApplication] - configLayer >>> appLayer + val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = + (HttpApplicationLive(_, _)).toLayer[HttpApplication] } -import HttpApplicationLive.AppConfig - case class HttpApplicationLive( config: AppConfig, - contextBuilder: (Request[AppTask], Config) => Http4sWebContext[AppTask] + security: HttpSecurity ) extends HttpApplication: - val dsl: Http4sDsl[AppTask] = new Http4sDsl[AppTask] {} import dsl.* - // TODO: zio-config - def oidcClient(): OidcClient = { - val oidcConfiguration = new OidcConfiguration() - oidcConfiguration.setClientId("mdrpdbtest") - oidcConfiguration.setSecret("aCZqYp2aGl1C2MbGDvglZXbJEUwRHV02") - oidcConfiguration.setDiscoveryURI( - "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration" - ) - oidcConfiguration.setUseNonce(true) - // oidcConfiguration.addCustomParam("prompt", "consent") - val oidcClient = new OidcClient(oidcConfiguration) - - val authorizationGenerator = new AuthorizationGenerator { - override def generate( - context: WebContext, - sessionStore: SessionStore, - profile: UserProfile - ): Optional[UserProfile] = { - profile.addRole("ROLE_ADMIN") - Optional.of(profile) - } - } - oidcClient.setAuthorizationGenerator(authorizationGenerator) - oidcClient - } - - val pac4jConfig = - val clients = - Clients(s"${config.urlBase}/mdr/pdb/auth/callback", oidcClient()) - val conf = org.pac4j.core.config.Config(clients) - conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) - conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) - conf - - private val sessionConfig = SessionConfig( - cookieName = "session", - mkCookie = ResponseCookie(_, _, path = Some("/")), - secret = "This is a secret", - maxAge = 5.minutes - ) - - val callbackService = - CallbackService[AppTask](pac4jConfig, contextBuilder) - - val localLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - Some(config.urlBase), - destroySession = true - ) - val centralLogoutService = LogoutService[AppTask]( - pac4jConfig, - contextBuilder, - defaultUrl = Some(config.urlBase), - logoutUrlPattern = Some(s"${config.urlBase}.*"), - localLogout = false, - destroySession = true, - centralLogout = true - ) - - def filesService(appPath: String): HttpRoutes[AppTask] = - ZHttp4sServerInterpreter() - .from( - List( - fileGetServerEndpoint("pdb" / "app")( - s"${appPath}/index.html" - ), - filesGetServerEndpoint("pdb")(appPath) - ) - ) - .toRoutes - - val smMW = Session.sessionManagement[AppTask](sessionConfig) - val sfMW = SecurityFilterMiddleware - .securityFilter[AppTask](pac4jConfig, contextBuilder) - - def authedProtectedPages(appPath: String): HttpRoutes[AppTask] = - smMW.compose(sfMW)( - filesService(appPath).local( - (req: ContextRequest[AppTask, List[CommonProfile]]) => req.req - ) - ) - - val rootRoutes: HttpRoutes[AppTask] = HttpRoutes.of { - case req @ GET -> Root / "callback" => - callbackService.callback(req) - case req @ POST -> Root / "callback" => - callbackService.callback(req) - case req @ GET -> Root / "logout" => - localLogoutService.logout(req) - case req @ GET -> Root / "centralLogout" => - centralLogoutService.logout(req) - } + val files = static.Routes(config) def httpApp(appPath: String): HttpRoutes[AppTask] = Router( - "/mdr/pdb/auth" -> smMW(rootRoutes), - "/mdr" -> authedProtectedPages(appPath) + security.route, + "/mdr" -> security.secure(files.routes) ) override def routes(): UIO[HttpRoutes[AppTask]] = diff --git a/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala new file mode 100644 index 0000000..43a864f --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/HttpSecurity.scala @@ -0,0 +1,8 @@ +package mdr.pdb.server + +import org.http4s.HttpRoutes +import org.http4s.server.AuthMiddleware + +trait HttpSecurity: + def route: (String, HttpRoutes[AppTask]) + def secure: AuthMiddleware[AppTask, AppAuth] diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index 1d78408..0b9a866 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -1,50 +1,6 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.HttpRoutes trait HttpServer: def serve(): UIO[ExitCode] - -object BlazeHttpServer { - import zio.config.* - - case class BlazeServerConf(host: String, port: Int) - - val blazeServerConfig: ConfigDescriptor[BlazeServerConf] = - import ConfigDescriptor.* - nested("BLAZE")( - string("HOST").default("localhost") zip int("PORT").default(8080) - ).to[BlazeServerConf] - - val layer: RLayer[System & HttpApplication, HttpServer] = - val configLayer = ZConfig.fromSystemEnv( - blazeServerConfig, - keyDelimiter = Some('_'), - valueDelimiter = Some(',') - ) - val routesLayer = ZLayer - .environment[HttpApplication] - .flatMap(a => ZLayer.fromZIO(a.get.routes())) - val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] - (configLayer ++ routesLayer) >>> blazeLayer -} - -import BlazeHttpServer.* - -case class BlazeHttpServer( - config: BlazeServerConf, - httpApp: HttpRoutes[AppTask] -) extends HttpServer: - override def serve(): UIO[ExitCode] = - BlazeServerBuilder[AppTask] - .bindHttp(config.port, config.host) - .withHttpApp(httpApp.orNotFound) - .serve - .compile - .drain - .fold(_ => ExitCode.failure, _ => ExitCode.success) - .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index 37f509d..5498b11 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -1,30 +1,26 @@ package mdr.pdb.server import zio.* -import zio.interop.catz.* -import zio.interop.catz.implicits.{*, given} -import org.http4s.Request -import org.pac4j.http4s.Http4sWebContext +import org.pac4j.core.profile.CommonProfile -type AppTask = RIO[ZEnv, *] +type AppEnv = ZEnv +type AppTask = RIO[AppEnv, *] +type AppAuth = List[CommonProfile] object Main extends ZIOAppDefault: - // TODO: move inside HttpApplication (using ZIO.runtime) - private val contextBuilder = - (req: Request[AppTask], conf: org.pac4j.core.config.Config) => - new Http4sWebContext[AppTask]( - req, - conf.getSessionStore, - runtime.unsafeRun(_) - ) + lazy val runtimeLayer = ZLayer.fromZIO(ZIO.runtime[AppEnv]) + lazy val securityLayer = + security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer + lazy val appLayer = + AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + lazy val serverLayer = + blaze.BlazeServerConfig.fromEnv >+> appLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] - .provideCustom( - HttpApplicationLive.layer(contextBuilder) >>> BlazeHttpServer.layer - ) + .provideCustom(serverLayer) _ <- server.serve() } yield () diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala new file mode 100644 index 0000000..cf8a89d --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.HttpRoutes + +object BlazeHttpServer { + val layer: URLayer[BlazeServerConfig & HttpApplication, HttpServer] = + val routesLayer = ZLayer + .environment[HttpApplication] + .flatMap(a => ZLayer.fromZIO(a.get.routes())) + val blazeLayer = (BlazeHttpServer(_, _)).toLayer[HttpServer] + (ZLayer.environment[BlazeServerConfig] ++ routesLayer) >>> blazeLayer +} + +import BlazeHttpServer.* + +case class BlazeHttpServer( + config: BlazeServerConfig, + httpApp: HttpRoutes[AppTask] +) extends HttpServer: + override def serve(): UIO[ExitCode] = + BlazeServerBuilder[AppTask] + .bindHttp(config.port, config.host) + .withHttpApp(httpApp.orNotFound) + .serve + .compile + .drain + .fold(_ => ExitCode.failure, _ => ExitCode.success) + .provideEnvironment(ZEnvironment.default) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala new file mode 100644 index 0000000..01232b8 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeServerConfig.scala @@ -0,0 +1,20 @@ +package mdr.pdb.server +package blaze + +import zio.* +import zio.config.* + +case class BlazeServerConfig(host: String, port: Int) + +object BlazeServerConfig: + val configDesc: ConfigDescriptor[BlazeServerConfig] = + import ConfigDescriptor.* + nested("BLAZE")( + string("HOST").default("localhost") zip int("PORT").default(8080) + ).to[BlazeServerConfig] + + val fromEnv = ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala b/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala new file mode 100644 index 0000000..b2e4e62 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/security/Pac4jHttpSecurity.scala @@ -0,0 +1,122 @@ +package mdr.pdb.server +package security + +import java.util.Optional +import org.http4s.HttpRoutes +import org.http4s.Request +import org.http4s.ResponseCookie +import org.http4s.server.AuthMiddleware +import org.pac4j.core.authorization.generator.AuthorizationGenerator +import org.pac4j.core.client.Clients +import org.pac4j.core.config.Config +import org.pac4j.core.context.WebContext +import org.pac4j.core.context.session.SessionStore +import org.pac4j.core.profile.CommonProfile +import org.pac4j.core.profile.UserProfile +import org.pac4j.http4s.* +import org.pac4j.oidc.client.OidcClient +import org.pac4j.oidc.config.OidcConfiguration +import scala.concurrent.duration.{*, given} +import zio.* +import zio.config.* +import zio.interop.catz.* +import zio.interop.catz.implicits.{*, given} + +object Pac4jHttpSecurity: + val layer: URLayer[Pac4jSecurityConfig & Runtime[AppEnv], HttpSecurity] = + (Pac4jHttpSecurity(_, _)).toLayer[HttpSecurity] + +class Pac4jHttpSecurity( + config: Pac4jSecurityConfig, + runtime: Runtime[AppEnv] +) extends HttpSecurity + with CustomDsl: + private val contextBuilder = + (req: Request[AppTask], conf: org.pac4j.core.config.Config) => + new Http4sWebContext[AppTask]( + req, + conf.getSessionStore, + runtime.unsafeRun(_) + ) + + def oidcClient(): OidcClient = { + val oidcConfiguration = new OidcConfiguration() + oidcConfiguration.setClientId(config.clientId) + oidcConfiguration.setSecret(config.clientSecret) + oidcConfiguration.setDiscoveryURI(config.discoveryURI) + oidcConfiguration.setUseNonce(true) + // oidcConfiguration.addCustomParam("prompt", "consent") + val oidcClient = new OidcClient(oidcConfiguration) + + val authorizationGenerator = new AuthorizationGenerator { + override def generate( + context: WebContext, + sessionStore: SessionStore, + profile: UserProfile + ): Optional[UserProfile] = { + // profile.addRole("ROLE_ADMIN") + Optional.of(profile) + } + } + oidcClient.setAuthorizationGenerator(authorizationGenerator) + oidcClient + } + + val pac4jConfig = + val clients = + Clients( + s"${config.urlBase}/${config.callbackBase}/callback", + oidcClient() + ) + val conf = org.pac4j.core.config.Config(clients) + conf.setHttpActionAdapter(DefaultHttpActionAdapter[AppTask]()) + conf.setSessionStore(Http4sCacheSessionStore[AppTask]()) + conf + + private val sessionConfig = SessionConfig( + cookieName = "session", + mkCookie = ResponseCookie(_, _, path = Some("/")), + secret = "This is a secret", + maxAge = 5.minutes + ) + + val callbackService = + CallbackService[AppTask](pac4jConfig, contextBuilder) + + val localLogoutService = LogoutService[AppTask]( + pac4jConfig, + contextBuilder, + config.logoutUrl, + destroySession = true + ) + val centralLogoutService = LogoutService[AppTask]( + pac4jConfig, + contextBuilder, + defaultUrl = config.logoutUrl, + logoutUrlPattern = Some(s"${config.logoutUrl}.*"), + localLogout = false, + destroySession = true, + centralLogout = true + ) + + val sessionManagement = Session.sessionManagement[AppTask](sessionConfig) + val securityFilter = SecurityFilterMiddleware + .securityFilter[AppTask](pac4jConfig, contextBuilder) + + val routes: HttpRoutes[AppTask] = + HttpRoutes.of { + case req @ GET -> Root / "callback" => + callbackService.callback(req) + case req @ POST -> Root / "callback" => + callbackService.callback(req) + case req @ GET -> Root / "logout" => + localLogoutService.logout(req) + case req @ GET -> Root / "centralLogout" => + centralLogoutService.logout(req) + } + + override val route: (String, HttpRoutes[AppTask]) = + s"/${config.callbackBase}" -> sessionManagement(routes) + + override def secure: AuthMiddleware[AppTask, List[CommonProfile]] = + sessionManagement.compose(securityFilter) diff --git a/server/src/main/scala/mdr/pdb/server/security/Pac4jSecurityConfig.scala b/server/src/main/scala/mdr/pdb/server/security/Pac4jSecurityConfig.scala new file mode 100644 index 0000000..72c1c7e --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/security/Pac4jSecurityConfig.scala @@ -0,0 +1,33 @@ +package mdr.pdb.server +package security + +import zio.* +import zio.config.* + +case class Pac4jSecurityConfig( + urlBase: String, + callbackBase: String, + logoutUrl: Option[String], + clientId: String, + clientSecret: String, + discoveryURI: String +) + +object Pac4jSecurityConfig: + val configDesc: ConfigDescriptor[Pac4jSecurityConfig] = + import ConfigDescriptor.* + nested("SECURITY")( + string("URLBASE") zip string("CALLBACKBASE") zip string( + "LOGOUTURL" + ).optional zip string("CLIENTID") zip string("CLIENTSECRET") + zip string( + "DISCOVERYURI" + ) + ).to[Pac4jSecurityConfig] + + val fromEnv: ZLayer[System, ReadError[String], Pac4jSecurityConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/src/main/scala/mdr/pdb/server/static/Routes.scala b/server/src/main/scala/mdr/pdb/server/static/Routes.scala new file mode 100644 index 0000000..d056a04 --- /dev/null +++ b/server/src/main/scala/mdr/pdb/server/static/Routes.scala @@ -0,0 +1,18 @@ +package mdr.pdb.server +package static + +import org.http4s.AuthedRoutes + +class Routes(config: AppConfig): + import CustomTapir.* + + val endpoints: List[ZServerEndpoint[AppEnv, Any]] = + List( + fileGetServerEndpoint("pdb" / "app")( + s"${config.appPath}/index.html" + ), + filesGetServerEndpoint("pdb")(config.appPath) + ) + + val routes: AuthedRoutes[AppAuth, AppTask] = + CustomTapir.from(endpoints).toRoutes.local(_.req)