mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:56:57 +00:00
838 lines
38 KiB
Scala
838 lines
38 KiB
Scala
package code.api.util
|
|
|
|
import java.text.SimpleDateFormat
|
|
import java.util.{Date, UUID}
|
|
|
|
import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessJson, PostConsentJson}
|
|
import code.api.util.ApiRole.{canCreateEntitlementAtAnyBank, canCreateEntitlementAtOneBank}
|
|
import code.api.v3_1_0.{PostConsentBodyCommonJson, PostConsentEntitlementJsonV310, PostConsentViewJsonV310}
|
|
import code.api.{Constant, RequestHeader}
|
|
import code.bankconnectors.Connector
|
|
import code.consent
|
|
import code.consent.{ConsentStatus, Consents, MappedConsent}
|
|
import code.consumer.Consumers
|
|
import code.context.{ConsentAuthContextProvider, UserAuthContextProvider}
|
|
import code.entitlement.Entitlement
|
|
import code.model.Consumer
|
|
import code.users.Users
|
|
import code.util.Helper.MdcLoggable
|
|
import code.util.HydraUtil
|
|
import code.views.Views
|
|
import com.nimbusds.jwt.JWTClaimsSet
|
|
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
|
import com.openbankproject.commons.model._
|
|
import net.liftweb.common.{Box, Empty, Failure, Full}
|
|
import net.liftweb.http.provider.HTTPParam
|
|
import net.liftweb.json.JsonParser.ParseException
|
|
import net.liftweb.json.{Extraction, MappingException, compactRender, parse}
|
|
import net.liftweb.mapper.By
|
|
import net.liftweb.util.{ControlHelpers, Props}
|
|
import sh.ory.hydra.model.OAuth2TokenIntrospection
|
|
|
|
import scala.collection.immutable.{List, Nil}
|
|
import scala.concurrent.Future
|
|
|
|
case class ConsentJWT(createdByUserId: String,
|
|
sub: String, // An identifier for the user, unique among all OBP-API users and never reused
|
|
iss: String, // The Issuer Identifier for the Issuer of the response.
|
|
aud: String, // Identifies the audience that this ID token is intended for. It must be one of the OBP-API client IDs of your application.
|
|
jti: String, // (JWT ID) claim provides a unique identifier for the JWT.(OBP use jti as consentId)
|
|
iat: Long, // The "iat" (issued at) claim identifies the time at which the JWT was issued. Represented in Unix time (integer seconds).
|
|
nbf: Long, // The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. Represented in Unix time (integer seconds).
|
|
exp: Long, // The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. Represented in Unix time (integer seconds).
|
|
name: Option[String],
|
|
email: Option[String],
|
|
entitlements: List[Role],
|
|
views: List[ConsentView],
|
|
access: Option[ConsentAccessJson]) {
|
|
def toConsent(): Consent = {
|
|
Consent(
|
|
createdByUserId=this.createdByUserId,
|
|
subject=this.sub,
|
|
issuer=this.iss,
|
|
consumerKey=this.aud,
|
|
consentId=this.jti, //OBP use jti as consentId
|
|
issuedAt=this.iat,
|
|
validFrom=this.nbf,
|
|
validTo=this.exp,
|
|
name=this.name,
|
|
email=this.email,
|
|
entitlements=this.entitlements,
|
|
views=this.views,
|
|
access = this.access
|
|
)
|
|
}
|
|
}
|
|
|
|
case class Role(role_name: String,
|
|
bank_id: String
|
|
)
|
|
case class ConsentView(bank_id: String,
|
|
account_id: String,
|
|
view_id : String
|
|
)
|
|
|
|
case class Consent(createdByUserId: String,
|
|
subject: String,
|
|
issuer: String,
|
|
consumerKey: String,
|
|
consentId: String,
|
|
issuedAt: Long,
|
|
validFrom: Long,
|
|
validTo: Long,
|
|
name: Option[String],
|
|
email: Option[String],
|
|
entitlements: List[Role],
|
|
views: List[ConsentView],
|
|
access: Option[ConsentAccessJson]
|
|
) {
|
|
def toConsentJWT(): ConsentJWT = {
|
|
ConsentJWT(
|
|
createdByUserId=this.createdByUserId,
|
|
sub=this.subject,
|
|
iss=this.issuer,
|
|
aud=this.consumerKey,
|
|
jti=this.consentId,
|
|
iat=this.issuedAt,
|
|
nbf=this.validFrom,
|
|
exp=this.validTo,
|
|
name=this.name,
|
|
email=this.email,
|
|
entitlements=this.entitlements,
|
|
views=this.views,
|
|
access = this.access
|
|
)
|
|
}
|
|
}
|
|
|
|
object Consent extends MdcLoggable {
|
|
|
|
final lazy val challengeAnswerAtTestEnvironment = "123"
|
|
|
|
/**
|
|
* Purpose of this helper function is to get the Consumer-Key value from a Request Headers.
|
|
* @return the Consumer-Key value from a Request Header as a String
|
|
*/
|
|
def getConsumerKey(requestHeaders: List[HTTPParam]): Option[String] = {
|
|
requestHeaders.toSet.filter(_.name == RequestHeader.`Consumer-Key`).toList match {
|
|
case x :: Nil => Some(x.values.mkString(", "))
|
|
case _ => None
|
|
}
|
|
}
|
|
/**
|
|
* Purpose of this helper function is to get the Consumer via MTLS info i.e. PEM certificate.
|
|
* @return the boxed Consumer
|
|
*/
|
|
def getCurrentConsumerViaMtls(callContext: CallContext): Box[Consumer] = {
|
|
val clientCert: String = APIUtil.`getPSD2-CERT`(callContext.requestHeaders)
|
|
.getOrElse(SecureRandomUtil.csprng.nextLong().toString)
|
|
def removeBreakLines(input: String) = input
|
|
.replace("\n", "")
|
|
.replace("\r", "")
|
|
Consumers.consumers.vend.getConsumerByPemCertificate(clientCert).or(
|
|
Consumers.consumers.vend.getConsumerByPemCertificate(removeBreakLines(clientCert))
|
|
)
|
|
}
|
|
|
|
private def verifyHmacSignedJwt(jwtToken: String, c: MappedConsent): Boolean = {
|
|
JwtUtil.verifyHmacSignedJwt(jwtToken, c.secret)
|
|
}
|
|
|
|
private def checkConsumerIsActiveAndMatched(consent: ConsentJWT, callContext: CallContext): Box[Boolean] = {
|
|
Consumers.consumers.vend.getConsumerByConsumerId(consent.aud) match {
|
|
case Full(consumerFromConsent) if consumerFromConsent.isActive.get == true => // Consumer is active
|
|
val validationMetod = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE")
|
|
if(validationMetod != "CONSUMER_CERTIFICATE" && Props.mode == Props.RunModes.Production) {
|
|
logger.warn(s"consumer_validation_method_for_consent is not set to CONSUMER_CERTIFICATE! The current value is: ${validationMetod}")
|
|
}
|
|
validationMetod match {
|
|
case "CONSUMER_KEY_VALUE" =>
|
|
val requestHeaderConsumerKey = getConsumerKey(callContext.requestHeaders)
|
|
requestHeaderConsumerKey match {
|
|
case Some(reqHeaderConsumerKey) =>
|
|
if (reqHeaderConsumerKey == consumerFromConsent.key.get)
|
|
Full(true) // This consent can be used by current application
|
|
else // This consent can NOT be used by current application
|
|
Failure(ErrorMessages.ConsentDoesNotMatchConsumer)
|
|
case None => Failure(ErrorMessages.ConsumerKeyHeaderMissing) // There is no header `Consumer-Key` in request headers
|
|
}
|
|
case "CONSUMER_CERTIFICATE" =>
|
|
val clientCert: String = APIUtil.`getPSD2-CERT`(callContext.requestHeaders).getOrElse(SecureRandomUtil.csprng.nextLong().toString)
|
|
def removeBreakLines(input: String) = input
|
|
.replace("\n", "")
|
|
.replace("\r", "")
|
|
if (removeBreakLines(clientCert) == removeBreakLines(consumerFromConsent.clientCertificate.get))
|
|
Full(true) // This consent can be used by current application
|
|
else // This consent can NOT be used by current application
|
|
Failure(ErrorMessages.ConsentDoesNotMatchConsumer)
|
|
case "NONE" => // This instance does not require validation method
|
|
Full(true)
|
|
case _ => // This instance does not specify validation method
|
|
Failure(ErrorMessages.ConsumerValidationMethodForConsentNotDefined)
|
|
}
|
|
case Full(consumer) if consumer.isActive.get == false => // Consumer is NOT active
|
|
Failure(ErrorMessages.ConsumerAtConsentDisabled + " aud: " + consent.aud)
|
|
case _ => // There is NO Consumer
|
|
Failure(ErrorMessages.ConsumerAtConsentCannotBeFound + " aud: " + consent.aud)
|
|
}
|
|
}
|
|
|
|
private def checkConsent(consent: ConsentJWT, consentIdAsJwt: String, callContext: CallContext): Box[Boolean] = {
|
|
Consents.consentProvider.vend.getConsentByConsentId(consent.jti) match {
|
|
case Full(c) if c.mStatus == ConsentStatus.ACCEPTED.toString | c.mStatus == ConsentStatus.VALID.toString =>
|
|
verifyHmacSignedJwt(consentIdAsJwt, c) match {
|
|
case true =>
|
|
(System.currentTimeMillis / 1000) match {
|
|
case currentTimeInSeconds if currentTimeInSeconds < consent.nbf =>
|
|
Failure(ErrorMessages.ConsentNotBeforeIssue)
|
|
case currentTimeInSeconds if currentTimeInSeconds > consent.exp =>
|
|
Failure(ErrorMessages.ConsentExpiredIssue)
|
|
case _ =>
|
|
checkConsumerIsActiveAndMatched(consent, callContext)
|
|
}
|
|
case false =>
|
|
Failure(ErrorMessages.ConsentVerificationIssue)
|
|
}
|
|
case Full(c) if c.mStatus != ConsentStatus.ACCEPTED.toString =>
|
|
Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.ACCEPTED.toString}.")
|
|
case _ =>
|
|
Failure(ErrorMessages.ConsentNotFound)
|
|
}
|
|
}
|
|
|
|
private def getOrCreateUser(subject: String, issuer: String, consentId: Option[String], name: Option[String], email: Option[String]): Future[(Box[User], Boolean)] = {
|
|
Users.users.vend.getOrCreateUserByProviderIdFuture(
|
|
provider = issuer,
|
|
idGivenByProvider = subject,
|
|
consentId = consentId,
|
|
name = name,
|
|
email = email
|
|
)
|
|
}
|
|
private def getOrCreateUserOldStyle(subject: String, issuer: String, consentId: Option[String], name: Option[String], email: Option[String]): Box[User] = {
|
|
Users.users.vend.getUserByProviderId(provider = issuer, idGivenByProvider = subject).or { // Find a user
|
|
Users.users.vend.createResourceUser( // Otherwise create a new one
|
|
provider = issuer,
|
|
providerId = Some(subject),
|
|
createdByConsentId = consentId,
|
|
name = name,
|
|
email = email,
|
|
userId = None,
|
|
createdByUserInvitationId = None,
|
|
company = None,
|
|
lastMarketingAgreementSignedDate = None
|
|
)
|
|
}
|
|
}
|
|
|
|
private def addEntitlements(user: User, consent: ConsentJWT): Box[User] = {
|
|
def addConsentEntitlements(existingEntitlements: List[Entitlement], entitlement: Role): (Role, String) = {
|
|
ApiRole.availableRoles.exists(_.toString == entitlement.role_name) match { // Check a role name
|
|
case true =>
|
|
val role = ApiRole.valueOf(entitlement.role_name)
|
|
existingEntitlements.exists(_.roleName == entitlement.role_name) match { // Check is a role already added to a user
|
|
case false =>
|
|
val bankId = if (role.requiresBankId) entitlement.bank_id else ""
|
|
Entitlement.entitlement.vend.addEntitlement(bankId, user.userId, entitlement.role_name) match {
|
|
case Full(_) => (entitlement, "AddedOrExisted")
|
|
case _ =>
|
|
(entitlement, "Cannot add the entitlement: " + entitlement)
|
|
}
|
|
case true =>
|
|
(entitlement, "AddedOrExisted")
|
|
}
|
|
case false =>
|
|
(entitlement, "There is no entitlement's name: " + entitlement)
|
|
}
|
|
}
|
|
|
|
val entitlements: List[Role] = consent.entitlements
|
|
Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId) match {
|
|
case Full(existingEntitlements) =>
|
|
val triedToAdd =
|
|
for {
|
|
entitlement <- entitlements
|
|
} yield {
|
|
addConsentEntitlements(existingEntitlements, entitlement)
|
|
}
|
|
val failedToAdd: List[(Role, String)] = triedToAdd.filter(_._2 != "AddedOrExisted")
|
|
failedToAdd match {
|
|
case Nil => Full(user)
|
|
case _ =>
|
|
Failure("The entitlements cannot be added. " + failedToAdd.map(i => (i._1, i._2)).mkString(", "))
|
|
}
|
|
case _ =>
|
|
Failure("Cannot get entitlements for user id: " + user.userId)
|
|
}
|
|
|
|
}
|
|
|
|
private def grantAccessToViews(user: User, consent: ConsentJWT): Box[User] = {
|
|
for {
|
|
view <- consent.views
|
|
} yield {
|
|
val bankIdAccountIdViewId = BankIdAccountIdViewId(BankId(view.bank_id), AccountId(view.account_id),ViewId(view.view_id))
|
|
Views.views.vend.revokeAccess(bankIdAccountIdViewId, user)
|
|
}
|
|
val result =
|
|
for {
|
|
view <- consent.views
|
|
} yield {
|
|
val bankIdAccountIdViewId = BankIdAccountIdViewId(BankId(view.bank_id), AccountId(view.account_id),ViewId(view.view_id))
|
|
Views.views.vend.systemView(ViewId(view.view_id)) match {
|
|
case Full(systemView) =>
|
|
Views.views.vend.grantAccessToSystemView(BankId(view.bank_id), AccountId(view.account_id), systemView, user)
|
|
case _ =>
|
|
// It's not system view
|
|
Views.views.vend.grantAccessToCustomView(bankIdAccountIdViewId, user)
|
|
}
|
|
"Added"
|
|
}
|
|
if (result.forall(_ == "Added")) Full(user) else Failure("Cannot add permissions to the user with id: " + user.userId)
|
|
}
|
|
|
|
private def applyConsentRulesCommonOldStyle(consentIdAsJwt: String, calContext: CallContext): Box[User] = {
|
|
implicit val dateFormats = CustomJsonFormats.formats
|
|
|
|
def applyConsentRules(consent: ConsentJWT): Box[User] = {
|
|
// 1. Get or Create a User
|
|
getOrCreateUserOldStyle(consent.sub, consent.iss, Some(consent.toConsent().consentId), None, None) match {
|
|
case (Full(user)) =>
|
|
// 2. Assign entitlements to the User
|
|
addEntitlements(user, consent) match {
|
|
case (Full(user)) =>
|
|
// 3. Assign views to the User
|
|
grantAccessToViews(user, consent)
|
|
case everythingElse =>
|
|
everythingElse
|
|
}
|
|
case _ =>
|
|
Failure("Cannot create or get the user based on: " + consentIdAsJwt)
|
|
}
|
|
}
|
|
|
|
JwtUtil.getSignedPayloadAsJson(consentIdAsJwt) match {
|
|
case Full(jsonAsString) =>
|
|
try {
|
|
val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]
|
|
checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-JWT expired
|
|
case (Full(true)) => // OK
|
|
applyConsentRules(consent)
|
|
case failure@Failure(_, _, _) => // Handled errors
|
|
failure
|
|
case _ => // Unexpected errors
|
|
Failure(ErrorMessages.ConsentCheckExpiredIssue)
|
|
}
|
|
} catch { // Possible exceptions
|
|
case e: ParseException => Failure("ParseException: " + e.getMessage)
|
|
case e: MappingException => Failure("MappingException: " + e.getMessage)
|
|
case e: Exception => Failure("parsing failed: " + e.getMessage)
|
|
}
|
|
case failure@Failure(_, _, _) =>
|
|
failure
|
|
case _ =>
|
|
Failure("Cannot extract data from: " + consentIdAsJwt)
|
|
}
|
|
}
|
|
|
|
private def applyConsentRulesCommon(consentAsJwt: String, callContext: CallContext): Future[(Box[User], Option[CallContext])] = {
|
|
implicit val dateFormats = CustomJsonFormats.formats
|
|
|
|
def applyConsentRules(consent: ConsentJWT): Future[(Box[User], Option[CallContext])] = {
|
|
val cc = callContext
|
|
// 1. Get or Create a User
|
|
getOrCreateUser(consent.sub, consent.iss, Some(consent.jti), None, None) map {
|
|
case (Full(user), newUser) =>
|
|
// 2. Assign entitlements to the User
|
|
addEntitlements(user, consent) match {
|
|
case (Full(user)) =>
|
|
// 3. Copy Auth Context to the User
|
|
copyAuthContextOfConsentToUser(consent.jti, user.userId, newUser) match {
|
|
case Full(_) =>
|
|
// 4. Assign views to the User
|
|
(grantAccessToViews(user, consent), Some(cc))
|
|
case failure@Failure(_, _, _) => // Handled errors
|
|
(failure, Some(callContext))
|
|
case _ =>
|
|
(Failure(ErrorMessages.UnknownError), Some(cc))
|
|
}
|
|
case failure@Failure(msg, exp, chain) => // Handled errors
|
|
(Failure(msg), Some(cc))
|
|
case _ =>
|
|
(Failure("Cannot add entitlements based on: " + consentAsJwt), Some(cc))
|
|
}
|
|
case _ =>
|
|
(Failure("Cannot create or get the user based on: " + consentAsJwt), Some(cc))
|
|
}
|
|
}
|
|
|
|
JwtUtil.getSignedPayloadAsJson(consentAsJwt) match {
|
|
case Full(jsonAsString) =>
|
|
try {
|
|
val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]
|
|
// Set Consumer into Call Context
|
|
val consumer = getCurrentConsumerViaMtls(callContext)
|
|
val updatedCallContext = callContext.copy(consumer = consumer)
|
|
checkConsent(consent, consentAsJwt, updatedCallContext) match { // Check is it Consent-JWT expired
|
|
case (Full(true)) => // OK
|
|
applyConsentRules(consent)
|
|
case failure@Failure(_, _, _) => // Handled errors
|
|
Future(failure, Some(updatedCallContext))
|
|
case _ => // Unexpected errors
|
|
Future(Failure(ErrorMessages.ConsentCheckExpiredIssue), Some(updatedCallContext))
|
|
}
|
|
} catch { // Possible exceptions
|
|
case e: ParseException => Future(Failure("ParseException: " + e.getMessage), Some(callContext))
|
|
case e: MappingException => Future(Failure("MappingException: " + e.getMessage), Some(callContext))
|
|
case e: Exception => Future(Failure("parsing failed: " + e.getMessage), Some(callContext))
|
|
}
|
|
case failure@Failure(_, _, _) =>
|
|
Future(failure, Some(callContext))
|
|
case _ =>
|
|
Future(Failure("Cannot extract data from: " + consentAsJwt), Some(callContext))
|
|
}
|
|
}
|
|
|
|
def applyRules(consentJwt: Option[String], callContext: CallContext): Future[(Box[User], Option[CallContext])] = {
|
|
val allowed = APIUtil.getPropsAsBoolValue(nameOfProperty="consents.allowed", defaultValue=false)
|
|
(consentJwt, allowed) match {
|
|
case (Some(jwt), true) => applyConsentRulesCommon(jwt, callContext)
|
|
case (_, false) => Future((Failure(ErrorMessages.ConsentDisabled), Some(callContext)))
|
|
case (None, _) => Future((Failure(ErrorMessages.ConsentHeaderNotFound), Some(callContext)))
|
|
}
|
|
}
|
|
|
|
def getConsentJwtValueByConsentId(consentId: String): Option[MappedConsent] = {
|
|
APIUtil.checkIfStringIsUUIDVersion1(consentId) match {
|
|
case true => // String is a UUID
|
|
Consents.consentProvider.vend.getConsentByConsentId(consentId) match {
|
|
case Full(consent) => Some(consent)
|
|
case _ => None // It's not valid UUID value
|
|
}
|
|
case false => None // It's not UUID at all
|
|
}
|
|
}
|
|
|
|
private def copyAuthContextOfConsentToUser(consentId: String, userId: String, newUser: Boolean): Box[List[UserAuthContext]] = {
|
|
if(newUser) {
|
|
val authContexts = ConsentAuthContextProvider.consentAuthContextProvider.vend.getConsentAuthContextsBox(consentId)
|
|
.map(_.map(i => BasicUserAuthContext(i.key, i.value)))
|
|
UserAuthContextProvider.userAuthContextProvider.vend.createOrUpdateUserAuthContexts(userId, authContexts.getOrElse(Nil))
|
|
} else {
|
|
Full(Nil)
|
|
}
|
|
}
|
|
private def applyBerlinGroupConsentRulesCommon(consentId: String, callContext: CallContext): Future[(Box[User], Option[CallContext])] = {
|
|
implicit val dateFormats = CustomJsonFormats.formats
|
|
|
|
def applyConsentRules(consent: ConsentJWT): Future[(Box[User], Option[CallContext])] = {
|
|
val cc = callContext
|
|
// 1. Get or Create a User
|
|
getOrCreateUser(consent.sub, consent.iss, Some(consent.toConsent().consentId), None, None) map {
|
|
case (Full(user), newUser) =>
|
|
// 2. Assign entitlements (Roles) to the User
|
|
addEntitlements(user, consent) match {
|
|
case Full(user) =>
|
|
// 3. Copy Auth Context to the User
|
|
copyAuthContextOfConsentToUser(consent.jti, user.userId, newUser) match {
|
|
case Full(_) =>
|
|
// 4. Assign views to the User
|
|
(grantAccessToViews(user, consent), Some(cc))
|
|
case failure@Failure(_, _, _) => // Handled errors
|
|
(failure, Some(callContext))
|
|
case _ =>
|
|
(Failure(ErrorMessages.UnknownError), Some(cc))
|
|
}
|
|
case failure@Failure(msg, exp, chain) => // Handled errors
|
|
(Failure(msg), Some(cc))
|
|
case _ =>
|
|
(Failure("Cannot add entitlements based on: " + consentId), Some(cc))
|
|
}
|
|
case _ =>
|
|
(Failure("Cannot create or get the user based on: " + consentId), Some(cc))
|
|
}
|
|
}
|
|
|
|
def checkFrequencyPerDay(storedConsent: consent.ConsentTrait) = {
|
|
def isSameDay(date1: Date, date2: Date): Boolean = {
|
|
val fmt = new SimpleDateFormat("yyyyMMdd")
|
|
fmt.format(date1).equals(fmt.format(date2))
|
|
}
|
|
var usesSoFarTodayCounter = storedConsent.usesSoFarTodayCounter
|
|
storedConsent.recurringIndicator match {
|
|
case false => // The consent is for one access to the account data
|
|
if(usesSoFarTodayCounter == 0) // Maximum value is "1".
|
|
(true, 0) // All good
|
|
else
|
|
(false, 1) // Exceeded rate limit
|
|
case true => // The consent is for recurring access to the account data
|
|
if(!isSameDay(storedConsent.usesSoFarTodayCounterUpdatedAt, new Date())) {
|
|
usesSoFarTodayCounter = 0 // Reset counter
|
|
}
|
|
if(usesSoFarTodayCounter < storedConsent.frequencyPerDay)
|
|
(true, usesSoFarTodayCounter) // All good
|
|
else
|
|
(false, storedConsent.frequencyPerDay) // Exceeded rate limit
|
|
}
|
|
}
|
|
|
|
// 1st we need to find a Consent via the field MappedConsent.consentId
|
|
Consents.consentProvider.vend.getConsentByConsentId(consentId) match {
|
|
case Full(storedConsent) =>
|
|
// Set Consumer into Call Context
|
|
val consumer = getCurrentConsumerViaMtls(callContext)
|
|
val updatedCallContext = callContext.copy(consumer = consumer)
|
|
// This function MUST be called only once per call. I.e. it's date dependent
|
|
val (canBeUsed, currentCounterState) = checkFrequencyPerDay(storedConsent)
|
|
if(canBeUsed) {
|
|
JwtUtil.getSignedPayloadAsJson(storedConsent.jsonWebToken) match {
|
|
case Full(jsonAsString) =>
|
|
try {
|
|
val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]
|
|
checkConsent(consent, storedConsent.jsonWebToken, updatedCallContext) match { // Check is it Consent-JWT expired
|
|
case (Full(true)) => // OK
|
|
// Update MappedConsent.usesSoFarTodayCounter field
|
|
Consents.consentProvider.vend.updateBerlinGroupConsent(consentId, currentCounterState + 1)
|
|
applyConsentRules(consent)
|
|
case failure@Failure(_, _, _) => // Handled errors
|
|
Future(failure, Some(updatedCallContext))
|
|
case _ => // Unexpected errors
|
|
Future(Failure(ErrorMessages.ConsentCheckExpiredIssue), Some(updatedCallContext))
|
|
}
|
|
} catch { // Possible exceptions
|
|
case e: ParseException => Future(Failure("ParseException: " + e.getMessage), Some(updatedCallContext))
|
|
case e: MappingException => Future(Failure("MappingException: " + e.getMessage), Some(updatedCallContext))
|
|
case e: Exception => Future(Failure("parsing failed: " + e.getMessage), Some(updatedCallContext))
|
|
}
|
|
case failure@Failure(_, _, _) =>
|
|
Future(failure, Some(updatedCallContext))
|
|
case _ =>
|
|
Future(Failure("Cannot extract data from: " + consentId), Some(updatedCallContext))
|
|
}
|
|
} else {
|
|
Future(Failure(ErrorMessages.TooManyRequests + s" ${RequestHeader.`Consent-ID`}: $consentId"), Some(updatedCallContext))
|
|
}
|
|
case failure@Failure(_, _, _) =>
|
|
Future(failure, Some(callContext))
|
|
case _ =>
|
|
Future(Failure(ErrorMessages.ConsentNotFound + s" ($consentId)"), Some(callContext))
|
|
}
|
|
}
|
|
def applyBerlinGroupRules(consentId: Option[String], callContext: CallContext): Future[(Box[User], Option[CallContext])] = {
|
|
val allowed = APIUtil.getPropsAsBoolValue(nameOfProperty="consents.allowed", defaultValue=false)
|
|
(consentId, allowed) match {
|
|
case (Some(consentId), true) => applyBerlinGroupConsentRulesCommon(consentId, callContext)
|
|
case (_, false) => Future((Failure(ErrorMessages.ConsentDisabled), Some(callContext)))
|
|
case (None, _) => Future((Failure(ErrorMessages.ConsentHeaderNotFound), Some(callContext)))
|
|
}
|
|
}
|
|
def applyRulesOldStyle(consentId: Option[String], callContext: CallContext): (Box[User], CallContext) = {
|
|
val allowed = APIUtil.getPropsAsBoolValue(nameOfProperty="consents.allowed", defaultValue=false)
|
|
(consentId, allowed) match {
|
|
case (Some(consentId), true) => (applyConsentRulesCommonOldStyle(consentId, callContext), callContext)
|
|
case (_, false) => (Failure(ErrorMessages.ConsentDisabled), callContext)
|
|
case (None, _) => (Failure(ErrorMessages.ConsentHeaderNotFound), callContext)
|
|
}
|
|
}
|
|
|
|
|
|
def createConsentJWT(user: User,
|
|
consent: PostConsentBodyCommonJson,
|
|
secret: String,
|
|
consentId: String,
|
|
consumerId: Option[String],
|
|
validFrom: Option[Date],
|
|
timeToLive: Long): String = {
|
|
|
|
lazy val currentConsumerId = Consumer.findAll(By(Consumer.createdByUserId, user.userId)).map(_.consumerId.get).headOption.getOrElse("")
|
|
val currentTimeInSeconds = System.currentTimeMillis / 1000
|
|
val timeInSeconds = validFrom match {
|
|
case Some(date) => date.getTime() / 1000
|
|
case _ => currentTimeInSeconds
|
|
}
|
|
// Write Consent's Auth Context to the DB
|
|
val authContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(user.userId)
|
|
.map(_.map(i => BasicUserAuthContext(i.key, i.value)))
|
|
ConsentAuthContextProvider.consentAuthContextProvider.vend.createOrUpdateConsentAuthContexts(consentId, authContexts.getOrElse(Nil))
|
|
|
|
// 1. Add views
|
|
// Please note that consents can only contain Views that the User already has access to.
|
|
val allUserViews = Views.views.vend.getPermissionForUser(user).map(_.views).getOrElse(Nil)
|
|
val views = consent.bank_id match {
|
|
case Some(bankId) =>
|
|
// Filter out roles for other banks
|
|
allUserViews.filterNot { i =>
|
|
!i.bankId.value.isEmpty() && i.bankId.value != bankId
|
|
}
|
|
case None =>
|
|
allUserViews
|
|
}
|
|
val viewsToAdd: Seq[ConsentView] =
|
|
for {
|
|
view <- views
|
|
if consent.everything || consent.views.exists(_ == PostConsentViewJsonV310(view.bankId.value,view.accountId.value, view.viewId.value))
|
|
} yield {
|
|
ConsentView(
|
|
bank_id = view.bankId.value,
|
|
account_id = view.accountId.value,
|
|
view_id = view.viewId.value
|
|
)
|
|
}
|
|
// 2. Add Roles
|
|
// Please note that consents can only contain Roles that the User already has access to.
|
|
val allUserEntitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).getOrElse(Nil)
|
|
val entitlements = consent.bank_id match {
|
|
case Some(bankId) =>
|
|
// Filter out roles for other banks
|
|
allUserEntitlements.filterNot { i =>
|
|
!i.bankId.isEmpty() && i.bankId != bankId
|
|
}
|
|
case None =>
|
|
allUserEntitlements
|
|
}
|
|
val entitlementsToAdd: Seq[Role] =
|
|
for {
|
|
entitlement <- entitlements
|
|
if !(entitlement.roleName == canCreateEntitlementAtOneBank.toString())
|
|
if !(entitlement.roleName == canCreateEntitlementAtAnyBank.toString())
|
|
if consent.everything || consent.entitlements.exists(_ == PostConsentEntitlementJsonV310(entitlement.bankId,entitlement.roleName))
|
|
} yield {
|
|
Role(entitlement.roleName, entitlement.bankId)
|
|
}
|
|
val json = ConsentJWT(
|
|
createdByUserId=user.userId,
|
|
sub=APIUtil.generateUUID(),
|
|
iss=Constant.HostName,
|
|
aud=consumerId.getOrElse(currentConsumerId),
|
|
jti=consentId,
|
|
iat=currentTimeInSeconds,
|
|
nbf=timeInSeconds,
|
|
exp=timeInSeconds + timeToLive,
|
|
name=None,
|
|
email=None,
|
|
entitlements=entitlementsToAdd.toList,
|
|
views=viewsToAdd.toList,
|
|
access = None
|
|
)
|
|
|
|
implicit val formats = CustomJsonFormats.formats
|
|
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
|
|
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
|
|
CertificateUtil.jwtWithHmacProtection(jwtClaims, secret)
|
|
}
|
|
|
|
def createBerlinGroupConsentJWT(user: Option[User],
|
|
consent: PostConsentJson,
|
|
secret: String,
|
|
consentId: String,
|
|
consumerId: Option[String],
|
|
validUntil: Option[Date]): Future[String] = {
|
|
|
|
val currentTimeInSeconds = System.currentTimeMillis / 1000
|
|
val validUntilTimeInSeconds = validUntil match {
|
|
case Some(date) => date.getTime() / 1000
|
|
case _ => currentTimeInSeconds
|
|
}
|
|
// Write Consent's Auth Context to the DB
|
|
user map { u =>
|
|
val authContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(u.userId)
|
|
.map(_.map(i => BasicUserAuthContext(i.key, i.value)))
|
|
ConsentAuthContextProvider.consentAuthContextProvider.vend.createOrUpdateConsentAuthContexts(consentId, authContexts.getOrElse(Nil))
|
|
}
|
|
|
|
// 1. Add access
|
|
val accounts: List[Future[ConsentView]] = consent.access.accounts.getOrElse(Nil) map { account =>
|
|
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), None) map { bankAccount =>
|
|
ConsentView(
|
|
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
|
|
account_id = bankAccount._1.map(_.accountId.value).getOrElse(""),
|
|
view_id = Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID
|
|
)
|
|
}
|
|
}
|
|
val balances: List[Future[ConsentView]] = consent.access.balances.getOrElse(Nil) map { account =>
|
|
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), None) map { bankAccount =>
|
|
ConsentView(
|
|
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
|
|
account_id = bankAccount._1.map(_.accountId.value).getOrElse(""),
|
|
view_id = Constant.SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID
|
|
)
|
|
}
|
|
}
|
|
val transactions: List[Future[ConsentView]] = consent.access.transactions.getOrElse(Nil) map { account =>
|
|
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), None) map { bankAccount =>
|
|
ConsentView(
|
|
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
|
|
account_id = bankAccount._1.map(_.accountId.value).getOrElse(""),
|
|
view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID
|
|
)
|
|
}
|
|
}
|
|
|
|
Future.sequence(accounts ::: balances ::: transactions) map { views =>
|
|
val json = ConsentJWT(
|
|
createdByUserId = user.map(_.userId).getOrElse(""),
|
|
sub = APIUtil.generateUUID(),
|
|
iss = Constant.HostName,
|
|
aud = consumerId.getOrElse(""),
|
|
jti = consentId,
|
|
iat = currentTimeInSeconds,
|
|
nbf = currentTimeInSeconds,
|
|
exp = validUntilTimeInSeconds,
|
|
name = None,
|
|
email = None,
|
|
entitlements = Nil,
|
|
views = views,
|
|
access = Some(consent.access)
|
|
)
|
|
implicit val formats = CustomJsonFormats.formats
|
|
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
|
|
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
|
|
CertificateUtil.jwtWithHmacProtection(jwtClaims, secret)
|
|
}
|
|
}
|
|
|
|
def createUKConsentJWT(
|
|
user: Option[User],
|
|
bankId: Option[String],
|
|
accountIds: Option[List[String]],
|
|
permissions: List[String],
|
|
expirationDateTime: Date,
|
|
transactionFromDateTime: Date,
|
|
transactionToDateTime: Date,
|
|
secret: String,
|
|
consentId: String,
|
|
consumerId: Option[String]
|
|
): String = {
|
|
|
|
val createdByUserId = user.map(_.userId).getOrElse("None")
|
|
val currentConsumerId = Consumer.findAll(By(Consumer.createdByUserId, createdByUserId)).map(_.consumerId.get).headOption.getOrElse("")
|
|
val currentTimeInSeconds = System.currentTimeMillis / 1000
|
|
val validUntilTimeInSeconds = expirationDateTime.getTime() / 1000
|
|
// Write Consent's Auth Context to the DB
|
|
user map { u =>
|
|
val authContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(u.userId)
|
|
.map(_.map(i => BasicUserAuthContext(i.key, i.value)))
|
|
ConsentAuthContextProvider.consentAuthContextProvider.vend.createOrUpdateConsentAuthContexts(consentId, authContexts.getOrElse(Nil))
|
|
}
|
|
|
|
// 1. Add views
|
|
val consentViews: List[ConsentView] = if (bankId.isDefined && accountIds.isDefined) {
|
|
permissions.map {
|
|
permission =>
|
|
accountIds.get.map(
|
|
accountId =>
|
|
ConsentView(
|
|
bank_id = bankId.getOrElse(null),
|
|
account_id = accountId,
|
|
view_id = permission
|
|
))
|
|
}.flatten
|
|
} else {
|
|
permissions.map {
|
|
permission =>
|
|
ConsentView(
|
|
bank_id = null,
|
|
account_id = null,
|
|
view_id = permission
|
|
)
|
|
}
|
|
}
|
|
|
|
val json = ConsentJWT(
|
|
createdByUserId = createdByUserId,
|
|
sub = APIUtil.generateUUID(),
|
|
iss = Constant.HostName,
|
|
aud = consumerId.getOrElse(currentConsumerId),
|
|
jti = consentId,
|
|
iat = currentTimeInSeconds,
|
|
nbf = currentTimeInSeconds,
|
|
exp = validUntilTimeInSeconds,
|
|
name = None,
|
|
email = None,
|
|
entitlements = Nil,
|
|
views = consentViews,
|
|
access = None
|
|
)
|
|
|
|
implicit val formats = CustomJsonFormats.formats
|
|
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
|
|
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
|
|
CertificateUtil.jwtWithHmacProtection(jwtClaims, secret)
|
|
}
|
|
|
|
private def checkConsumerIsActiveAndMatchedUK(consent: ConsentJWT, consumerIdOfLoggedInUser: Option[String]): Box[Boolean] = {
|
|
Consumers.consumers.vend.getConsumerByConsumerId(consent.aud) match {
|
|
case Full(consumerFromConsent) if consumerFromConsent.isActive.get == true => // Consumer is active
|
|
consumerIdOfLoggedInUser match {
|
|
case Some(consumerId) =>
|
|
if (consumerId == consumerFromConsent.consumerId.get)
|
|
Full(true) // This consent can be used by current application
|
|
else // This consent can NOT be used by current application
|
|
Failure(ErrorMessages.ConsentDoesNotMatchConsumer)
|
|
case None => Failure(ErrorMessages.ConsumerNotFound) // Consumer cannot be found by logged in user
|
|
}
|
|
case Full(consumerFromConsent) if consumerFromConsent.isActive.get == false => // Consumer is NOT active
|
|
Failure(ErrorMessages.ConsumerAtConsentDisabled + " aud: " + consent.aud)
|
|
case _ => // There is NO Consumer
|
|
Failure(ErrorMessages.ConsumerAtConsentCannotBeFound + " aud: " + consent.aud)
|
|
}
|
|
}
|
|
|
|
|
|
def checkUKConsent(user: User, calContext: Option[CallContext]): Box[Boolean] = {
|
|
val accessToken = calContext.flatMap(_.authReqHeaderField)
|
|
.map(_.replaceFirst("Bearer\\s+", ""))
|
|
.getOrElse(throw new RuntimeException("Not found http request header 'Authorization', it is mandatory."))
|
|
val introspectOAuth2Token: OAuth2TokenIntrospection = HydraUtil.hydraAdmin.introspectOAuth2Token(accessToken, null)
|
|
if(!introspectOAuth2Token.getActive) {
|
|
return Failure(ErrorMessages.ConsentExpiredIssue)
|
|
}
|
|
|
|
val boxedConsent: Box[MappedConsent] = {
|
|
val accessExt = introspectOAuth2Token.getExt.asInstanceOf[java.util.Map[String, String]]
|
|
val consentId = accessExt.get("consent_id")
|
|
Consents.consentProvider.vend.getConsentByConsentId(consentId)
|
|
}
|
|
|
|
boxedConsent match {
|
|
case Full(c) if c.mStatus == ConsentStatus.AUTHORISED.toString =>
|
|
System.currentTimeMillis match {
|
|
case currentTimeMillis if currentTimeMillis < c.creationDateTime.getTime =>
|
|
Failure(ErrorMessages.ConsentNotBeforeIssue)
|
|
case _ =>
|
|
val consumerIdOfLoggedInUser: Option[String] = calContext.flatMap(_.consumer.map(_.consumerId.get))
|
|
implicit val dateFormats = CustomJsonFormats.formats
|
|
val consent: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(c.jsonWebToken)
|
|
.map(parse(_).extract[ConsentJWT])
|
|
checkConsumerIsActiveAndMatchedUK(
|
|
consent.openOrThrowException("Parsing of the consent failed."),
|
|
consumerIdOfLoggedInUser
|
|
)
|
|
}
|
|
case Full(c) if c.mStatus != ConsentStatus.AUTHORISED.toString =>
|
|
Failure(s"${ErrorMessages.ConsentStatusIssue}${ConsentStatus.AUTHORISED.toString}.")
|
|
case _ =>
|
|
Failure(ErrorMessages.ConsentNotFound)
|
|
}
|
|
}
|
|
|
|
|
|
def filterByBankId(consents: List[MappedConsent], bankId: BankId): List[MappedConsent] = {
|
|
implicit val formats = CustomJsonFormats.formats
|
|
val consentsOfBank =
|
|
consents.filter { consent =>
|
|
val jsonWebTokenAsCaseClass: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken)
|
|
.map(parse(_).extract[ConsentJWT])
|
|
jsonWebTokenAsCaseClass match {
|
|
case Full(consentJWT) if consentJWT.entitlements.exists(_.bank_id.isEmpty()) => true// System roles
|
|
case Full(consentJWT) if consentJWT.entitlements.map(_.bank_id).contains(bankId.value) => true // Bank level roles
|
|
case Full(consentJWT) if consentJWT.views.map(_.bank_id).contains(bankId.value) => true
|
|
case _ => false
|
|
}
|
|
}
|
|
consentsOfBank
|
|
}
|
|
|
|
}
|