import scala.
collection.
mutable.
Map import scala.
concurrent.
Future import scala.
util.
{Failure, Success
} import scala.
util.
control.
NonFatal import scala.
collection.
JavaConversions.
_ import scala.
reflect.
runtime.
universe.
_ import play.
api.
Play.
configuration import play.
api.
libs.
concurrent.
Execution.
Implicits.
defaultContext import play.
api.
http.
{HeaderNames, HttpVerbs
} import utils.
common.
ReflectHelper.
_ import utils.
common.
Responses.
_ import utils.
auth.
AuthErrors.
_ import models.
auth.
{Token, PassToken
} import models.
auth.
TokenType.
_
/**
* Represents a secured request.
*
* @constructor Initializes a new instance of the [[SecuredRequest]] class.
* @param token The JSON Web Token.
* @param request The current HTTP request.
*/
request
: Request
[A
]) extends WrappedRequest
[A
](request
) {
/**
* Applies `authorized` if `condition` evaluates to `true`.
*
* @param condition The function that determines whether or not the
* request is authorized.
* @param authorized The function that is applied if `condition` evaluates
* to `true`.
* @return A `Future` value containing the response to be sent to
* the client.
* @note `condition` shall return a tuple containing a Boolean
* value indicating whether or not the request is authorized,
* and optionally a `Result` to be sent to the client if
* authorization is not granted and the subject does not
* have ''admin'' privileges.
* @note `authorized` takes a Boolean paramenter indicating
* whether the action is being authorized as ''admin''.
*/
condition: Token => Future[(Boolean, Option[Result])],
authorized: Boolean => Future[Result]
): Future[Result] = {
condition
(token
).
flatMap { r
=> r.
_1
match { Logger.debug(s"extra condition to authorize request ${request.uri} met")
case Some
(simpleResult
) => Logger.debug(s"extra condition to authorize request ${request.uri} not met")
Future.successful(simpleResult)
if (token.
roles.
contains(Role.
Admin.
id)) { Logger.debug(
s"extra condition to authorize request ${request.uri} not met, still " +
s"authorization is granted since user ${token.subject} has admin privileges"
)
} else Future.
successful(refuse
) }
Logger.error("error authorizing secured request", e)
InternalServerError(error(errorProcessingRequest(e.getMessage)(request)))
}
}
/**
* Refuses this request.
* @return A `Future` value containing the response to be sent to the client.
*/
Logger.debug(s"extra condition to authorize request ${request.uri} not met")
val user
= if (token.
tokenType == Browse
) None
else Some
(token.
subject) Unauthorized(error(requestNotAuthorized(user)(request)))
}
}
/**
* Represents a secured action.
*
* @constructor Initializes a new instance of the [[SecuredAction]] class.
* @tparam T The controller type to create a secured action for.
* @param methodName The name of the method to secure.
* @param tokenTypes One or more of the [[TokenType]] values.
*/
import services.
auth.
AuthPlugin
def invokeBlock
[A
](request
: Request
[A
], block
: SecuredRequest
[A
] => Future
[Result
]) = {{ if (tokenTypes.
length == 1 && tokenTypes
(0) == Pass
) None
else { request.
headers.
get(HeaderNames.
AUTHORIZATION) match { case Some
(header
) => s
"""$AuthScheme (.*)""".
r.
unapplySeq(header
).
map(_.
head.
trim) }
val auth
= jwt.
split(":") // auth(0) => token, auth(1) => signature val f
= AuthPlugin.
token(auth
(0))
/*
* service tokens (e.g. activation, reset) are discarded immediately after they
* trigger the action they are associated with
*/
f.onComplete {
case Success
(token
) => if (token.
tokenType != Authorization
&& request.
method != HttpVerbs.
GET) { AuthPlugin.discardToken(token.id)
}
case Failure
(e
) => Logger.
error("error deserializing token from request header", e
) }
f.flatMap {
case token
if (!token.
isValid) => Logger.warn(s"request ${request.uri} refused: token ${token.id} has been tampered")
Future.successful(Forbidden(error(authenticationViolated(token.subject)(request))))
case token
if (token.
isExpired) => Logger.debug(s"request ${request.uri} refused: token ${token.id} has expired")
Future.successful(Forbidden(error(authenticationExpired(token.subject)(request))))
case token
if (!isAuthorized
(token
)) => if (token.
tokenType == Browse
) { Logger.info(s"request ${request.uri} not authorized: anonym user does not have required privileges")
Future.successful(Unauthorized(error(requestNotAuthorized(None)(request))))
Logger.info(s"request ${request.uri} not authorized: user ${token.subject} does not have required privileges")
Future.successful(Unauthorized(error(requestNotAuthorized(Some(token.subject))(request))))
}
case token
=> if (token.
apiKey == Token.
BootstrapKey) { Logger.debug(s"request ${request.uri} authorized for user ${token.subject}")
block
(new SecuredRequest
(token, request
)) case json
: JsValue
=> json.
toString }
Secret
(token.
apiKey).
sign(auth
(0) + request.
method + request.
uri + body
) match { case Success
(signed
) => if (signed
== auth
(1)) { if (token.
tokenType == Browse
) { Logger.debug(s"request ${request.uri} authorized for anonym user")
Logger.debug(s"request ${request.uri} authorized for user ${token.subject}")
}
block
(new SecuredRequest
(token, request
)) Logger.warn(s"request ${request.uri} refused: request has been tampered")
Future.successful(Forbidden(error(requestViolated()(request))))
}
case Failure
(e
) => Future.
failed(e
) }
}
}.recover {
case e
: java.
text.
ParseException => Logger.error("error deserializing token from request header", e)
BadRequest(error(errorDeserializingToken(e.getMessage)(request)))
Logger.error("error processing secured request", e)
InternalServerError(error(errorProcessingRequest(e.getMessage)(request)))
}
if (tokenTypes.
contains(Pass
)) { Logger.debug(s"request ${request.uri} authorized for anonym user")
block
(new SecuredRequest
(PassToken
()(request
), request
)) Logger.debug(s"request ${request.uri} not authorized")
Future.successful(Unauthorized(
error(requestNotAuthenticated()(request))
).withHeaders(HeaderNames.WWW_AUTHENTICATE -> AuthScheme))
}
}
}
}
/**
* Returns a Boolean value indicating whether or not this
* action is authorized.
*
* @param token The authorization JSON Web Token.
* @return `true` if `token` defines the claims that authorize
* this action; otherwise, `false`.
*/
val roles
= securityProfiles
(operationNickname
[T
]) tokenTypes.contains(token.tokenType) && (
roles.contains(Role.id("any")) || // action does not require any specific privileges
token.roles.contains(Role.Admin.id) || // current subject has admin privileges
token.roles.exists(roles.contains(_)) // current subject has required privileges
)
}
/**
* Returns the fully qualified nickname of the method that handles
* the current HTTP request.
*
* @tparam T The type of the current `Controller`.
* @return The fully qualified nickname of the method that handles
* the current HTTP request.
*/
private def operationNickname
[T
<: Controller
: TypeTag
] = { val clazzAnnotations
= classAnnotations
[T
] val api
= clazzAnnotations
( "Api" )( "value"
).asInstanceOf[LiteralArgument].value.value.asInstanceOf[String]
val functionAnnotations
= methodAnnotations
[T
] val nickname
= functionAnnotations
(methodName
)( "ApiOperation" )( "nickname"
).asInstanceOf[LiteralArgument].value.value.asInstanceOf[String]
s"$api/$nickname"
}
}
/**
* Factory class for creating [[SecuredAction]] instances.
*/
private var securityProfiles
= Map
[String, List
[Int
]]().
withDefaultValue(List.
empty)
configuration.getConfigList("auth.securityProfiles").map { _.toList.map { config =>
config.getString("operation").map { operation =>
securityProfiles += (operation -> config.getStringList("roles").map {
_.toList.map(Role.id(_))
}.getOrElse(List.empty))
}
}}
/**
* Initializes a new instance of the [[SecuredAction]] class.
*
* @tparam T The controller type to create a secured action for.
* @param methodName The name of the method to secure.
*/
def apply
[T
<: Controller
: TypeTag
]( methodName: String
): SecuredAction[T] = apply(methodName, Authorization)
/**
* Initializes a new instance of the [[SecuredAction]] class.
*
* @tparam T The controller type to create a secured action for.
* @param methodName The name of the method to secure.
* @param tokenTypes One or more of the [[TokenType]] values.
*/
def apply
[T
<: Controller
: TypeTag
]( methodName: String,
tokenTypes: TokenType*
): SecuredAction
[T
] = new SecuredAction
[T
](methodName, tokenTypes
) }