OBP-API/obp-api/src/main/scala/code/api/util/ConsentUtil.scala
2024-06-17 09:37:36 +02:00

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
}
}