diff --git a/.travis.yml b/.travis.yml index c808293..9ee8225 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,16 @@ language: scala scala: - - 2.11.8 + - 2.11.12 + - 2.12.8 jdk: - oraclejdk8 script: - - sbt clean coverage test + - sbt clean +coverage +test after_success: - - sbt coverageReport coveralls + - sbt +coverageReport coveralls - bash <(curl -s https://codecov.io/bash) # go faster on travis @@ -17,5 +18,4 @@ sudo: false notifications: email: - - dmitry.krivaltsevich@zalando.de - - cezary.lada.extern@zalando.de \ No newline at end of file + - dmitry.krivaltsevich@zalando.de \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3937541..f893c17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -## [Unreleased] +## [2.7.0] - 2019-03-28 ### Changed - Rename `PlugableMetrics` trait to `PluggableMetrics`. Rename all occurrences of `Plugable*` to `Pluggable*`. - Add `Duration` support in configuration properties. +- Add support of Play! 2.7.0 ## [0.3.4] - 2018-06-06 ### Changed diff --git a/MAINTAINERS b/MAINTAINERS index f4314ee..55f12c3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,3 +1,2 @@ Dmitry Krivaltsevich -William Okuyama -Raymond Chenon \ No newline at end of file +Mikhail Litvin diff --git a/README.md b/README.md index 2def76c..b1246e7 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,7 @@ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/fa1fb822bc1246508d343880c0b1868c)](https://www.codacy.com/app/dmitrykrivaltsevich/play-zhewbacca?utm_source=github.com&utm_medium=referral&utm_content=zalando-stups/play-zhewbacca&utm_campaign=Badge_Grade) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/zalando-stups/play-zhewbacca/master/LICENSE) - -**[Table of Contents](http://tableofcontent.eu)** - -- [Play-security library](#play-security-library) - - [Core Technical Concepts](#core-technical-concepts) - - [Known alternatives](#known-alternatives) - - [Getting Started](#getting-started) - - [Contributing](#contributing) - - [Contact](#contact) - - [License](#license) - -Play! (v2.5 - 2.6) library to protect RESTful resource servers using OAuth2. According to [RFC 6749](https://tools.ietf.org/html/rfc6749#section-7): +Play! library to protect RESTful resource servers using OAuth2. Supported Play! Framework versions are 2.5.x, 2.6.x, 2.7.x. According to [RFC 6749](https://tools.ietf.org/html/rfc6749#section-7): > The client accesses protected resources by presenting the _access token_ to the _resource server_. The resource server MUST _validate_ the @@ -67,7 +56,7 @@ We mainly decided to release this library because we saw the needs of OAuth2 pro Configure libraries dependencies in your `build.sbt`: ```scala -libraryDependencies += "org.zalando" %% "play-zhewbacca" % "0.3.4" +libraryDependencies += "org.zalando" %% "play-zhewbacca" % "2.7.0" ``` To configure Development environment: diff --git a/build.sbt b/build.sbt index d534c2e..98b8391 100644 --- a/build.sbt +++ b/build.sbt @@ -3,12 +3,16 @@ import sbt.Keys._ import scalariform.formatter.preferences._ +val playFrameworkVersion = "2.7.0" + val commonSettings = Seq( organization := "org.zalando", - version := "0.3.4", - scalaVersion := "2.12.5", - crossScalaVersions := Seq("2.11.12", "2.12.5"), - scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8"), + version := playFrameworkVersion, + scalaVersion := "2.12.8", + // play 2.7.0 started to use scalac version 2.13.0-M5, but + // we cannot use "2.13.0-M5" because of "atmos" library (it does not support scala 2.13 yet) + crossScalaVersions := Seq("2.11.12", "2.12.8"), + scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8", "-Xfatal-warnings"), publishTo := { val nexus = "https://oss.sonatype.org/" if (isSnapshot.value) { @@ -27,12 +31,10 @@ val commonSettings = Seq( ) ) -val playFrameworkVersion = "2.6.12" - lazy val testDependencies = Seq( - "org.specs2" %% "specs2-core" % "4.0.3" % "test", - "org.specs2" %% "specs2-junit" % "4.0.3" % "test" + "org.specs2" %% "specs2-core" % "4.3.5" % "test", + "org.specs2" %% "specs2-junit" % "4.3.5" % "test" ) lazy val playDependencies = @@ -104,9 +106,6 @@ pomExtra := ( Dmitry Krivaltsevich - William Okuyama - - - Raymond Chenon + Mikhail Litvin ) diff --git a/project/build.properties b/project/build.properties index b7dd3cb..72f9028 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.0.2 +sbt.version=1.2.7 diff --git a/src/main/scala/org/zalando/zhewbacca/SecurityFilter.scala b/src/main/scala/org/zalando/zhewbacca/SecurityFilter.scala index ebec27c..2e8fbcf 100644 --- a/src/main/scala/org/zalando/zhewbacca/SecurityFilter.scala +++ b/src/main/scala/org/zalando/zhewbacca/SecurityFilter.scala @@ -23,9 +23,11 @@ class SecurityFilter @Inject() ( implicit val mat: Materializer, implicit val ec: ExecutionContext) extends Filter { + private val logger = Logger(getClass) + override def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = { rulesRepository.get(requestHeader).getOrElse { - Logger.debug(s"No security rules found for ${requestHeader.method} ${requestHeader.uri}. Access denied.") + logger.debug(s"No security rules found for ${requestHeader.method} ${requestHeader.uri}. Access denied.") DenyAllRule }.execute(nextFilter, requestHeader) } diff --git a/src/main/scala/org/zalando/zhewbacca/SecurityRulesRepository.scala b/src/main/scala/org/zalando/zhewbacca/SecurityRulesRepository.scala index 523630f..2cac70c 100644 --- a/src/main/scala/org/zalando/zhewbacca/SecurityRulesRepository.scala +++ b/src/main/scala/org/zalando/zhewbacca/SecurityRulesRepository.scala @@ -11,6 +11,8 @@ import play.api.mvc.RequestHeader class SecurityRulesRepository @Inject() (configuration: Configuration, provider: AuthProvider) { + private val logger = Logger(getClass) + private val SupportedHttpMethods: Set[String] = Set(GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) private val ConfigKeyMethod = "method" @@ -26,7 +28,7 @@ class SecurityRulesRepository @Inject() (configuration: Configuration, provider: private def load(): Seq[StrictRule] = { val securityRulesFileName = configuration.getOptional[String]("authorisation.rules.file").getOrElse("security_rules.conf") - Logger.info(s"Configuration file for security rules: $securityRulesFileName") + logger.info(s"Configuration file for security rules: $securityRulesFileName") if (configFileExists(securityRulesFileName)) { ConfigFactory.load(securityRulesFileName) @@ -40,15 +42,15 @@ class SecurityRulesRepository @Inject() (configuration: Configuration, provider: private def toRule(config: Config): StrictRule = { (getHttpMethod(config), config.getString(ConfigKeyPathRegex), getAllowedFlag(config), getScopeNames(config)) match { case (Some(method), pathRegex, Some(true), _) => - Logger.info(s"Explicitly allowed unauthorized requests for method: '$method' and path regex: '$pathRegex'") + logger.info(s"Explicitly allowed unauthorized requests for method: '$method' and path regex: '$pathRegex'") ExplicitlyAllowedRule(method, pathRegex) case (Some(method), pathRegex, Some(false), _) => - Logger.info(s"Explicitly denied all requests for method: '$method' and path regex: '$pathRegex'") + logger.info(s"Explicitly denied all requests for method: '$method' and path regex: '$pathRegex'") ExplicitlyDeniedRule(method, pathRegex) case (Some(method), pathRegex, None, Some(scopeNames)) => - Logger.info(s"Configured required scopes '$scopeNames' for method '$method' and path regex: '$pathRegex'") + logger.info(s"Configured required scopes '$scopeNames' for method '$method' and path regex: '$pathRegex'") ValidateTokenRule(provider, method, pathRegex, Scope(scopeNames)) case _ => diff --git a/src/test/scala/org/zalando/zhewbacca/IAMClientSpec.scala b/src/test/scala/org/zalando/zhewbacca/IAMClientSpec.scala index 29da6e5..9bd0694 100644 --- a/src/test/scala/org/zalando/zhewbacca/IAMClientSpec.scala +++ b/src/test/scala/org/zalando/zhewbacca/IAMClientSpec.scala @@ -1,12 +1,15 @@ package org.zalando.zhewbacca import akka.actor.ActorSystem +import javax.inject.{Inject, Provider} import org.specs2.mutable.Specification import org.zalando.zhewbacca.metrics.NoOpPluggableMetrics import play.api.http.{DefaultFileMimeTypes, FileMimeTypesConfiguration, Port} +import play.api.inject._ import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.ws.WSClient import play.api.mvc._ +import play.api.routing.Router import play.api.test.WsTestClient import play.api.{Application, Configuration, Mode} import play.core.server.Server @@ -176,17 +179,14 @@ class IAMClientSpec extends Specification { } def fakeApp(delay: Duration = 0.second, response: Result = Results.Ok): Application = { - val routes: PartialFunction[(String, String), Handler] = { - case ("GET", "/tokeninfo") => Action { - Thread.sleep(delay.toMillis) - response - } - } - new GuiceApplicationBuilder() .in(Mode.Test) - .routes(routes) .configure("play.akka.actor-system" -> s"application_iam_client_${java.util.UUID.randomUUID}") + .bindings( + bind[Duration].toInstance(delay), + bind[Result].toInstance(response)) + .overrides( + bind[Router].toProvider[IAMClientTestRouterProvider]) .build } @@ -210,3 +210,16 @@ class IAMClientSpec extends Specification { new IAMClient(clientConfig, new NoOpPluggableMetrics, client, actorSystem, ExecutionContext.Implicits.global) } } + +class IAMClientTestRouterProvider @Inject() (components: ControllerComponents, delay: Duration, response: Result) extends Provider[Router] { + + import components.{actionBuilder => Action} + import play.api.routing.sird._ + + override def get(): Router = Router.from { + case GET(p"/tokeninfo") => Action { + Thread.sleep(delay.toMillis) + response + } + } +} diff --git a/src/test/scala/org/zalando/zhewbacca/SecurityFilterSpec.scala b/src/test/scala/org/zalando/zhewbacca/SecurityFilterSpec.scala index 3c34f5f..12d22ed 100644 --- a/src/test/scala/org/zalando/zhewbacca/SecurityFilterSpec.scala +++ b/src/test/scala/org/zalando/zhewbacca/SecurityFilterSpec.scala @@ -1,9 +1,11 @@ package org.zalando.zhewbacca +import javax.inject.{Inject, Provider} import play.api.inject._ import play.api.inject.guice.GuiceApplicationBuilder import play.api.mvc.Results._ import play.api.mvc._ +import play.api.routing.Router import play.api.test.{FakeRequest, PlaySpecification} import play.api.{Application, Mode} @@ -11,23 +13,11 @@ class SecurityFilterSpec extends PlaySpecification with BodyParsers { val testTokenInfo = TokenInfo("", Scope.Empty, "token type", "user uid", realm = "/employees") - val routes: PartialFunction[(String, String), Handler] = { - // test action returning action type. Shows the usage and makes it possible to test basic behaviour - // security rules described in 'security_filter.conf' file - case ("GET", "/") => Action { request => - import TokenInfoConverter._ - Ok(request.tokenInfo.tokenType) - } - - case ("GET", "/unprotected") => Action { - Ok - } - } - def appWithRoutes: Application = new GuiceApplicationBuilder() .in(Mode.Test) .bindings(bind[AuthProvider] to new AlwaysPassAuthProvider(testTokenInfo)) - .routes(routes) + .overrides( + bind[Router].toProvider[SecurityFilterTestRouterProvider]) .configure( "play.http.filters" -> "org.zalando.zhewbacca.TestingFilters", "authorisation.rules.file" -> "security_filter.conf") @@ -48,4 +38,23 @@ class SecurityFilterSpec extends PlaySpecification with BodyParsers { } +} + +class SecurityFilterTestRouterProvider @Inject() (components: ControllerComponents) extends Provider[Router] { + + import components.{actionBuilder => Action} + import play.api.routing.sird._ + + override def get(): Router = Router.from { + // test action returning action type. Shows the usage and makes it possible to test basic behaviour + // security rules described in 'security_filter.conf' file + case GET(p"/") => Action { request => + import TokenInfoConverter._ + Ok(request.tokenInfo.tokenType) + } + + case GET(p"/unprotected") => Action { + Ok + } + } } \ No newline at end of file