Skip to content
This repository has been archived by the owner on Sep 29, 2023. It is now read-only.

Commit

Permalink
Merge pull request #36 from matterche/migrate-to-play-26
Browse files Browse the repository at this point in the history
Upgrade to Play Framework 2.6.3 and Scala 2.12.3
  • Loading branch information
dmitrykrivaltsevich authored Sep 21, 2017
2 parents 4b74e81 + d15d17f commit 8c369b4
Show file tree
Hide file tree
Showing 18 changed files with 126 additions and 123 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- [Contact](#contact)
- [License](#license)

Play! (v2.5) library to protect RESTful resource servers using OAuth2. According to [RFC 6749](https://tools.ietf.org/html/rfc6749#section-7):
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):

> The client accesses protected resources by presenting the _access
token_ to the _resource server_. The resource server MUST _validate_ the
Expand Down
37 changes: 20 additions & 17 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,47 @@ import scalariform.formatter.preferences._
val commonSettings = Seq(
organization := "org.zalando",
version := "0.2.3",
scalaVersion := "2.11.8",
scalaVersion := "2.12.3",
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8"),
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value)
if (isSnapshot.value) {
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
else {
Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
},
publishMavenStyle := true,
publishArtifact in Test := false,
pomIncludeRepository := { _ => false },
resolvers := Seq(
"scalaz-bintray" at "http://dl.bintray.com/scalaz/releases",
"scalaz-bintray" at "http://dl.bintray.com/scalaz/releases",
"scoverage-bintray" at "https://dl.bintray.com/sksamuel/sbt-plugins/"
)
)

val playFrameworkVersion = "2.5.9"
val playFrameworkVersion = "2.6.5"

lazy val testDependencies =
Seq(
"org.specs2" %% "specs2-core" % "3.6.4" % "test",
"org.specs2" %% "specs2-junit" % "3.6.4" % "test"
"org.specs2" %% "specs2-core" % "3.9.5" % "test",
"org.specs2" %% "specs2-junit" % "3.9.5" % "test"
)

lazy val playDependencies =
Seq(
"com.typesafe.play" %% "play-json" % playFrameworkVersion,
"com.typesafe.play" %% "play-ws" % playFrameworkVersion,
"com.typesafe.play" %% "play" % playFrameworkVersion,
"com.typesafe.play" %% "play-test" % playFrameworkVersion % "test",
"com.typesafe.play" %% "play-specs2" % playFrameworkVersion % "test"
"com.typesafe.play" %% "play-ahc-ws" % playFrameworkVersion,
"com.typesafe.play" %% "play-json" % playFrameworkVersion,
"com.typesafe.play" %% "play-ws" % playFrameworkVersion,
"com.typesafe.play" %% "play" % playFrameworkVersion,
"com.typesafe.play" %% "play-test" % playFrameworkVersion % "test",
"com.typesafe.play" %% "play-specs2" % playFrameworkVersion % "test"
)

lazy val libraries =
Seq(
"io.zman" %% "atmos" % "2.1"
"io.paradoxical" %% "atmos" % "2.2"
)

lazy val root = (project in file("."))
Expand All @@ -64,14 +67,14 @@ scalastyleFailOnError := true
// Create a default Scala style task to run with tests
lazy val compileScalastyle = taskKey[Unit]("compileScalastyle")

compileScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Compile).toTask("").value
compileScalastyle := scalastyle.in(Compile).toTask("").value

(compileInputs in(Compile, compile)) <<= (compileInputs in(Compile, compile)) dependsOn compileScalastyle

scalariformSettings
scalariformSettings(autoformat = true)

ScalariformKeys.preferences := ScalariformKeys.preferences.value
.setPreference(DoubleIndentClassDeclaration, true)
.setPreference(DoubleIndentConstructorArguments, true)
.setPreference(PlaceScaladocAsterisksBeneathSecondAsterisk, true)
.setPreference(PreserveSpaceBeforeArguments, false)
.setPreference(AlignSingleLineCaseStatements, false)
Expand Down
8 changes: 4 additions & 4 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// style plugins
addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.5.1")
addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "0.7.0")
addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.0")
addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "1.0.0")

// code coverage
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.5")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")

addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
54 changes: 22 additions & 32 deletions src/main/scala/org/zalando/zhewbacca/IAMClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ class IAMClient @Inject() (
plugableMetrics: PlugableMetrics,
ws: WSClient,
actorSystem: ActorSystem,
implicit val ec: ExecutionContext
) extends ((OAuth2Token) => Future[Option[TokenInfo]]) {
implicit val ec: ExecutionContext) extends ((OAuth2Token) => Future[Option[TokenInfo]]) {

val logger: Logger = Logger("security.IAMClient")

Expand All @@ -44,51 +43,48 @@ class IAMClient @Inject() (
circuitStatus.get
}

val authEndpoint = config.getString("authorisation.iam.endpoint").getOrElse(
throw new IllegalArgumentException("Authorisation: IAM endpoint is not configured")
)
val authEndpoint = config.getOptional[String]("authorisation.iam.endpoint").getOrElse(
throw new IllegalArgumentException("Authorisation: IAM endpoint is not configured"))

val breakerMaxFailures = config.getInt("authorisation.iam.cb.maxFailures").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker max failures is not configured")
)
val breakerMaxFailures = config.getOptional[Int]("authorisation.iam.cb.maxFailures").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker max failures is not configured"))

val breakerCallTimeout = config.getInt("authorisation.iam.cb.callTimeout").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker call timeout is not configured")
).millis
val breakerCallTimeout = config.getOptional[Int]("authorisation.iam.cb.callTimeout").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker call timeout is not configured")).millis

val breakerResetTimeout = config.getInt("authorisation.iam.cb.resetTimeout").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker reset timeout is not configured")
).millis
val breakerResetTimeout = config.getOptional[Int]("authorisation.iam.cb.resetTimeout").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker reset timeout is not configured")).millis

val breakerMaxRetries = config.getInt("authorisation.iam.maxRetries").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker max retries is not configured")
).attempts
val breakerMaxRetries = config.getOptional[Int]("authorisation.iam.maxRetries").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker max retries is not configured")).attempts

val breakerRetryBackoff = config.getInt("authorisation.iam.retry.backoff.duration").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker the duration of exponential backoff is not configured")
).millis
val breakerRetryBackoff = config.getOptional[Int]("authorisation.iam.retry.backoff.duration").getOrElse(
throw new IllegalArgumentException("Authorisation: Circuit Breaker the duration of exponential backoff is not configured")).millis

lazy val breaker: CircuitBreaker = new CircuitBreaker(
actorSystem.scheduler,
breakerMaxFailures,
breakerCallTimeout,
breakerResetTimeout
).onHalfOpen {
breakerResetTimeout).onHalfOpen {
circuitStatus.set(METRICS_BREAKER_OPEN)
}.onOpen {
circuitStatus.set(METRICS_BREAKER_OPEN)
}.onClose {
circuitStatus.set(METRICS_BREAKER_CLOSED)
}

implicit val retryRecover = retryFor { breakerMaxRetries } using {
exponentialBackoff { breakerRetryBackoff }
} monitorWith {
logger.logger onRetrying logNothing onInterrupted logWarning onAborted logError
}

override def apply(token: OAuth2Token): Future[Option[TokenInfo]] = {
breaker.withCircuitBreaker(
plugableMetrics.timing(
retryAsync(s"Calling $authEndpoint") {
ws.url(authEndpoint).withQueryString(("access_token", token.value)).get()
}
)
).map { response =>
ws.url(authEndpoint).withQueryStringParameters(("access_token", token.value)).get()
})).map { response =>
response.status match {
case OK => Some(response.json.as[TokenInfo])
case _ => None
Expand All @@ -100,10 +96,4 @@ class IAMClient @Inject() (
}
}

implicit val retryRecover = retryFor { breakerMaxRetries } using {
exponentialBackoff { breakerRetryBackoff }
} monitorWith {
logger.logger onRetrying logNothing onInterrupted logWarning onAborted logError
}

}
7 changes: 3 additions & 4 deletions src/main/scala/org/zalando/zhewbacca/OAuth2AuthProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ package org.zalando.zhewbacca
import javax.inject.{Inject, Singleton}

import play.api.Logger
import play.api.libs.concurrent.Execution.Implicits._

import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}

/**
* Authorization provider which uses Zalando's IAM API to verify given OAuth2 token.
*/
@Singleton
class OAuth2AuthProvider @Inject() (getTokenInfo: (OAuth2Token) => Future[Option[TokenInfo]])
extends AuthProvider {
class OAuth2AuthProvider @Inject() (getTokenInfo: (OAuth2Token) => Future[Option[TokenInfo]])(implicit ec: ExecutionContext)
extends AuthProvider {

val logger: Logger = Logger("security.OAuth2AuthProvider")

Expand Down
5 changes: 2 additions & 3 deletions src/main/scala/org/zalando/zhewbacca/RequestValidator.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package org.zalando.zhewbacca

import play.api.Logger
import play.api.libs.concurrent.Execution.Implicits._
import play.api.mvc._

import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal

private[zhewbacca] object RequestValidator {

val logger: Logger = Logger(this.getClass)

def validate[A](scope: Scope, requestHeader: RequestHeader, authProvider: AuthProvider): Future[Either[Result, TokenInfo]] = {
def validate[A](scope: Scope, requestHeader: RequestHeader, authProvider: AuthProvider)(implicit ec: ExecutionContext): Future[Either[Result, TokenInfo]] = {
authProvider.valid(OAuth2Token.from(requestHeader), scope).map {
case AuthTokenValid(tokenInfo) => Right(tokenInfo)
case AuthTokenInvalid => Left(Results.Forbidden)
Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/org/zalando/zhewbacca/SecurityFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import scala.concurrent.{ExecutionContext, Future}
class SecurityFilter @Inject() (
rulesRepository: SecurityRulesRepository,
implicit val mat: Materializer,
implicit val ec: ExecutionContext
) extends Filter {
implicit val ec: ExecutionContext) extends Filter {

override def apply(nextFilter: (RequestHeader) => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
rulesRepository.get(requestHeader).getOrElse {
Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/org/zalando/zhewbacca/SecurityRule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ abstract class StrictRule(method: String, pathRegex: String) extends SecurityRul
abstract case class ValidateTokenRule(
method: String,
pathRegex: String,
scope: Scope
) extends StrictRule(method, pathRegex) {
scope: Scope) extends StrictRule(method, pathRegex) {

def authProvider: AuthProvider
private[this] val log = Logger(this.getClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package org.zalando.zhewbacca
import javax.inject.Inject

import com.typesafe.config.{Config, ConfigFactory}
import play.api.mvc.RequestHeader
import play.api.{Configuration, Logger}
import scala.collection.JavaConverters._

import scala.collection.JavaConversions._
import play.api.http.HttpVerbs._
import play.api.mvc.RequestHeader

class SecurityRulesRepository @Inject() (configuration: Configuration, provider: AuthProvider) {

Expand All @@ -25,12 +25,12 @@ class SecurityRulesRepository @Inject() (configuration: Configuration, provider:
rules.find(_.isApplicableTo(requestHeader))

private def load(): Seq[StrictRule] = {
val securityRulesFileName = configuration.getString("authorisation.rules.file").getOrElse("security_rules.conf")
val securityRulesFileName = configuration.getOptional[String]("authorisation.rules.file").getOrElse("security_rules.conf")
Logger.info(s"Configuration file for security rules: $securityRulesFileName")

if (configFileExists(securityRulesFileName)) {
ConfigFactory.load(securityRulesFileName)
.getConfigList(ConfigKeyRules)
.getConfigList(ConfigKeyRules).asScala
.map(toRule)
} else {
sys.error(s"configuration file $securityRulesFileName for security rules not found")
Expand Down Expand Up @@ -82,7 +82,7 @@ class SecurityRulesRepository @Inject() (configuration: Configuration, provider:

private def getScopeNames(config: Config): Option[Set[String]] = {
if (config.hasPath(ConfigKeyScopes)) {
Some(config.getStringList(ConfigKeyScopes).toSet)
Some(config.getStringList(ConfigKeyScopes).asScala.toSet)
} else {
None
}
Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/org/zalando/zhewbacca/TokenInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ object TokenInfo {
(JsPath \ "access_token").read[String] and
(JsPath \ "scope").read[Seq[String]].map(names => Scope(Set(names: _*))) and
(JsPath \ "token_type").read[String] and
(JsPath \ "uid").read[String]
)(TokenInfo.apply _)
(JsPath \ "uid").read[String])(TokenInfo.apply _)
}
31 changes: 16 additions & 15 deletions src/main/scala/org/zalando/zhewbacca/TokenInfoConverter.scala
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
package org.zalando.zhewbacca

import play.api.libs.typedmap.TypedKey
import play.api.mvc.RequestHeader

object TokenInfoConverter {

implicit class AuthenticatedRequestHeader(underlying: RequestHeader) {
private val AccessTokenKey: TypedKey[String] = TypedKey("tokenInfo.access_token")
private val ScopeKey: TypedKey[String] = TypedKey("tokenInfo.scope")
private val ScopeSeparator = '|'
private val TokenTypeKey: TypedKey[String] = TypedKey("tokenInfo.token_type")
private val UidKey: TypedKey[String] = TypedKey("tokenInfo.uid")

private val AccessTokenKey = "tokenInfo.access_token"
private val ScopeKey = "tokenInfo.scope"
private val ScopeSeparator = '|'
private val TokenTypeKey = "tokenInfo.token_type"
private val UidKey = "tokenInfo.uid"
implicit class AuthenticatedRequestHeader(underlying: RequestHeader) {

def tokenInfo: TokenInfo = {
val accessToken = underlying.tags.getOrElse(AccessTokenKey, sys.error("access token not provided"))
val scopeNames = underlying.tags.getOrElse(ScopeKey, sys.error("scope not provided"))
val accessToken = underlying.attrs.get(AccessTokenKey).getOrElse(sys.error("access token not provided"))
val scopeNames = underlying.attrs.get(ScopeKey).getOrElse(sys.error("scope not provided"))
.split(ScopeSeparator)
.toSet
val tokenType = underlying.tags.getOrElse(TokenTypeKey, sys.error("token type is not provided"))
val uid = underlying.tags.getOrElse(UidKey, sys.error("user id is not provided"))
val tokenType = underlying.attrs.get(TokenTypeKey).getOrElse(sys.error("token type is not provided"))
val uid = underlying.attrs.get(UidKey).getOrElse(sys.error("user id is not provided"))

TokenInfo(accessToken, Scope(scopeNames), tokenType, uid)
}

private[zhewbacca] def withTokenInfo(tokenInfo: TokenInfo): RequestHeader = {
private[zhewbacca] def withTokenInfo(tok: TokenInfo): RequestHeader = {
underlying
.withTag(AccessTokenKey, tokenInfo.accessToken)
.withTag(ScopeKey, tokenInfo.scope.names.mkString(ScopeSeparator.toString))
.withTag(TokenTypeKey, tokenInfo.tokenType)
.withTag(UidKey, tokenInfo.userUid)
.addAttr(AccessTokenKey, tok.accessToken)
.addAttr(ScopeKey, tok.scope.names.mkString(ScopeSeparator.toString))
.addAttr(TokenTypeKey, tok.tokenType)
.addAttr(UidKey, tok.userUid)
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/test/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->
<configuration>

<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{"yyyy-MM-dd HH:mm:ss,SSS"} {%level} [%.20thread] [%mdc{X-Flow-ID:--}] [%logger{40}] %message%n%xException</pattern>
</encoder>
</appender>

<appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT" />
</appender>

<logger name="play" level="INFO" />
<logger name="application" level="DEBUG" />

<root level="INFO">
<appender-ref ref="ASYNCSTDOUT" />
</root>

</configuration>
Loading

0 comments on commit 8c369b4

Please sign in to comment.