Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:OpenBankProject/OBP-API into dev…
Browse files Browse the repository at this point in the history
…elop
  • Loading branch information
simonredfern committed Dec 18, 2024
2 parents 12f6372 + ee437df commit adfb813
Show file tree
Hide file tree
Showing 34 changed files with 900 additions and 132 deletions.
2 changes: 2 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,8 @@ featured_apis=elasticSearchWarehouseV300
# -- Redis cache -------------------------------------
# cache.redis.url=127.0.0.1
# cache.redis.port=6379
# Default value is empty or omitted props
# cache.redis.password =
# ---------------------------------------------------------

# -- New Style Endpoints -----------------------
Expand Down
10 changes: 7 additions & 3 deletions obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ object OAuth2Login extends RestHelper with MdcLoggable {
Keycloak.applyRules(value, cc)
} else if (UnknownProvider.isIssuer(value)) {
UnknownProvider.applyRules(value, cc)
} else {
} else if (HydraUtil.integrateWithHydra) {
Hydra.applyRules(value, cc)
} else {
(Failure(Oauth2IsNotRecognized), Some(cc))
}
case false =>
(Failure(Oauth2IsNotAllowed), Some(cc))
Expand All @@ -106,10 +108,12 @@ object OAuth2Login extends RestHelper with MdcLoggable {
Azure.applyIdTokenRulesFuture(value, cc)
} else if (Keycloak.isIssuer(value)) {
Keycloak.applyRulesFuture(value, cc)
} else if (UnknownProvider.isIssuer(value)) {
} else if (UnknownProvider.isIssuer(value)) {
UnknownProvider.applyRulesFuture(value, cc)
} else if (HydraUtil.integrateWithHydra) {
UnknownProvider.applyRulesFuture(value, cc)
} else {
Hydra.applyRulesFuture(value, cc)
Future(Failure(Oauth2IsNotRecognized), Some(cc))
}
case false =>
Future((Failure(Oauth2IsNotAllowed), Some(cc)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2293,12 +2293,19 @@ object SwaggerDefinitionsJSON {
transaction_request_types = List(transactionRequestTypeJSONV210)
)

val transactionRequestAttributeJsonV400 = TransactionRequestAttributeJsonV400(
name = transactionRequestAttributeNameExample.value,
attribute_type = transactionRequestAttributeTypeExample.value,
value = transactionRequestAttributeValueExample.value
)

val transactionRequestBodyCounterpartyJSON = TransactionRequestBodyCounterpartyJSON(
counterpartyIdJson,
amountOfMoneyJsonV121,
"A description for the transaction to the counterparty",
description = "A description for the transaction to the counterparty",
chargePolicyExample.value,
Some(futureDateExample.value)
Some(futureDateExample.value),
Some(List(transactionRequestAttributeJsonV400))
)

val transactionRequestBodySEPAJSON = TransactionRequestBodySEPAJSON(
Expand Down Expand Up @@ -4752,12 +4759,6 @@ object SwaggerDefinitionsJSON {
`type` = transactionRequestAttributeTypeExample.value,
value = transactionRequestAttributeValueExample.value
)

val transactionRequestAttributeJsonV400 = TransactionRequestAttributeJsonV400(
name = transactionRequestAttributeNameExample.value,
`type` = transactionRequestAttributeTypeExample.value,
value = transactionRequestAttributeValueExample.value
)

val transactionRequestAttributesResponseJson = TransactionRequestAttributesResponseJson(
transaction_request_attributes = List(transactionRequestAttributeResponseJson)
Expand Down Expand Up @@ -4863,7 +4864,8 @@ object SwaggerDefinitionsJSON {
start_date = DateWithDayExampleObject,
end_date = DateWithDayExampleObject,
challenges = List(challengeJsonV400),
charge = transactionRequestChargeJsonV200
charge = transactionRequestChargeJsonV200,
attributes=Some(List(bankAttributeBankResponseJsonV400)),
)

val postSimpleCounterpartyJson400 = PostSimpleCounterpartyJson400(
Expand Down
8 changes: 6 additions & 2 deletions obp-api/src/main/scala/code/api/cache/Redis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import scalacache.memoization.{cacheKeyExclude, memoize, memoizeSync}
import scalacache.{Flags, ScalaCache}
import scalacache.redis.RedisCache
import scalacache.serialization.Codec

import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig}

import java.net.URI
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
import java.io.FileInputStream
import java.security.KeyStore
import com.typesafe.config.{Config, ConfigFactory}
import net.liftweb.common.Full

import scala.concurrent.Future
import scala.concurrent.duration.Duration
Expand All @@ -26,7 +27,10 @@ object Redis extends MdcLoggable {
val url = APIUtil.getPropsValue("cache.redis.url", "127.0.0.1")
val port = APIUtil.getPropsAsIntValue("cache.redis.port", 6379)
val timeout = 4000
val password: String = null // Replace with password if authentication is needed
val password: String = APIUtil.getPropsValue("cache.redis.password") match {
case Full(password) if password.trim.nonEmpty => password
case _ => null
}
val useSsl = APIUtil.getPropsAsBoolValue("redis.use.ssl", false)

final val poolConfig = new JedisPoolConfig()
Expand Down
1 change: 1 addition & 0 deletions obp-api/src/main/scala/code/api/constant/constant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ object RequestHeader {
final lazy val `Consent-JWT` = "Consent-JWT"
final lazy val `PSD2-CERT` = "PSD2-CERT"
final lazy val `If-None-Match` = "If-None-Match"
final lazy val `TPP-Redirect-URL` = "TPP-Redirect-URL"
/**
* The If-Modified-Since request HTTP header makes the request conditional:
* the server sends back the requested resource, with a 200 status,
Expand Down
8 changes: 8 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiRole.scala
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,14 @@ object ApiRole extends MdcLoggable{
case class CanGetAccountsMinimalForCustomerAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetAccountsMinimalForCustomerAtAnyBank = CanGetAccountsMinimalForCustomerAtAnyBank()

case class CanUpdateConsentStatusAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canUpdateConsentStatusAtOneBank = CanUpdateConsentStatusAtOneBank()
case class CanUpdateConsentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
lazy val canUpdateConsentStatusAtAnyBank = CanUpdateConsentStatusAtAnyBank()
case class CanUpdateConsentAccountAccessAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canUpdateConsentAccountAccessAtOneBank = CanUpdateConsentAccountAccessAtOneBank()
case class CanUpdateConsentAccountAccessAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
lazy val canUpdateConsentAccountAccessAtAnyBank = CanUpdateConsentAccountAccessAtAnyBank()
case class CanRevokeConsentAtBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canRevokeConsentAtBank = CanRevokeConsentAtBank()
case class CanGetConsentsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
Expand Down
74 changes: 65 additions & 9 deletions obp-api/src/main/scala/code/api/util/ConsentUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ 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}
Expand Down Expand Up @@ -40,6 +39,7 @@ case class ConsentJWT(createdByUserId: String,
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).
request_headers: List[HTTPParam],
name: Option[String],
email: Option[String],
entitlements: List[Role],
Expand All @@ -55,8 +55,9 @@ case class ConsentJWT(createdByUserId: String,
issuedAt=this.iat,
validFrom=this.nbf,
validTo=this.exp,
name=this.name,
email=this.email,
request_headers=this.request_headers,
name=this.name,
email=this.email,
entitlements=this.entitlements,
views=this.views,
access = this.access
Expand All @@ -80,6 +81,7 @@ case class Consent(createdByUserId: String,
issuedAt: Long,
validFrom: Long,
validTo: Long,
request_headers: List[HTTPParam],
name: Option[String],
email: Option[String],
entitlements: List[Role],
Expand All @@ -96,6 +98,7 @@ case class Consent(createdByUserId: String,
iat=this.issuedAt,
nbf=this.validFrom,
exp=this.validTo,
request_headers=this.request_headers,
name=this.name,
email=this.email,
entitlements=this.entitlements,
Expand Down Expand Up @@ -650,6 +653,7 @@ object Consent extends MdcLoggable {
iat=currentTimeInSeconds,
nbf=timeInSeconds,
exp=timeInSeconds + timeToLive,
request_headers = Nil,
name=None,
email=None,
entitlements=entitlementsToAdd.toList,
Expand Down Expand Up @@ -714,7 +718,7 @@ object Consent extends MdcLoggable {
)
}
}

val tppRedirectUrl: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Redirect-URL`)
Future.sequence(accounts ::: balances ::: transactions) map { views =>
val json = ConsentJWT(
createdByUserId = user.map(_.userId).getOrElse(""),
Expand All @@ -725,19 +729,67 @@ object Consent extends MdcLoggable {
iat = currentTimeInSeconds,
nbf = currentTimeInSeconds,
exp = validUntilTimeInSeconds,
request_headers = tppRedirectUrl.toList,
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)
Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, secret))
}
}
def updateBerlinGroupConsentJWT(access: ConsentAccessJson,
consent: MappedConsent,
callContext: Option[CallContext]): Future[Box[String]] = {
implicit val dateFormats = CustomJsonFormats.formats
val payloadToUpdate: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) // Payload as JSON string
.map(net.liftweb.json.parse(_).extract[ConsentJWT]) // Extract case class


// 1. Add access
val accounts: List[Future[ConsentView]] = access.accounts.getOrElse(Nil) map { account =>
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $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]] = access.balances.getOrElse(Nil) map { account =>
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.balances.bankAccount: $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]] = access.transactions.getOrElse(Nil) map { account =>
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.transactions.bankAccount: $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 =>
if(views.isEmpty) {
Empty
} else {
implicit val formats = CustomJsonFormats.formats
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
val updatedPayload = payloadToUpdate.map(i => i.copy(views = views)) // Update only the field "views"
val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload))
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, secret))
Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret))
}
}
}
Expand Down Expand Up @@ -798,6 +850,7 @@ object Consent extends MdcLoggable {
iat = currentTimeInSeconds,
nbf = currentTimeInSeconds,
exp = validUntilTimeInSeconds,
request_headers = Nil,
name = None,
email = None,
entitlements = Nil,
Expand Down Expand Up @@ -875,9 +928,12 @@ object Consent extends MdcLoggable {
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
// Views
case Full(consentJWT) if consentJWT.views.isEmpty => true // There is no IAM
case Full(consentJWT) if consentJWT.views.map(_.bank_id).contains(bankId.value) => true
// Roles
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 _ => false
}
}
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ object ErrorMessages {
val Oauth2TokenHaveNoConsumer = "OBP-20209: The token have no linked consumer. "
val Oauth2TokenMatchCertificateFail = "OBP-20210: The token is linked with a different client certificate. "
val Oauth2TokenEndpointAuthMethodForbidden = "OBP-20213: The Token Endpoint Auth Method is not supported at this instance: "

val OneTimePasswordExpired = "OBP-20211: The One Time Password (OTP) has expired. "
val Oauth2IsNotRecognized = "OBP-20214: OAuth2 Access Token is not recognised at this instance."

// X.509
val X509GeneralError = "OBP-20300: PEM Encoded Certificate issue."
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/util/ExampleValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ object ExampleValue {
lazy val transactionRequestAttributeNameExample = ConnectorField("HOUSE_RENT", s"Transaction Request attribute name")
glossaryItems += makeGlossaryItem("Transaction Requests.attributeName", transactionRequestAttributeNameExample)

lazy val transactionRequestAttributeTypeExample = ConnectorField("DATE_WITH_DAY", s"Transaction Request attribute type.")
lazy val transactionRequestAttributeTypeExample = ConnectorField("STRING", s"Transaction Request attribute type.")
glossaryItems += makeGlossaryItem("Transaction Requests.attributeType", transactionRequestAttributeTypeExample)

lazy val transactionRequestAttributeValueExample = ConnectorField("123456789", s"Transaction Request attribute value.")
Expand Down
32 changes: 28 additions & 4 deletions obp-api/src/main/scala/code/api/util/NewStyle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3174,17 +3174,39 @@ object NewStyle extends MdcLoggable{
}
}

def getTransactionRequestIdsByAttributeNameValues(bankId: BankId, params: Map[String, List[String]],
callContext: Option[CallContext]): OBPReturnType[List[String]] = {
def getTransactionRequestIdsByAttributeNameValues(
bankId: BankId,
params: Map[String, List[String]],
isPersonal: Boolean,
callContext: Option[CallContext]
): OBPReturnType[List[String]] = {
Connector.connector.vend.getTransactionRequestIdsByAttributeNameValues(
bankId: BankId,
params: Map[String, List[String]],
isPersonal,
callContext: Option[CallContext]
) map {
i => (connectorEmptyResponse(i._1, callContext), i._2)
}
}


def getByAttributeNameValues(
bankId: BankId,
params: Map[String, List[String]],
isPersonal: Boolean,
callContext: Option[CallContext]
): OBPReturnType[List[TransactionRequestAttributeTrait]] = {
Connector.connector.vend.getByAttributeNameValues(
bankId: BankId,
params: Map[String, List[String]],
isPersonal,
callContext
) map {
i => (connectorEmptyResponse(i._1, callContext), i._2)
}
}

def createOrUpdateTransactionRequestAttribute(bankId: BankId,
transactionRequestId: TransactionRequestId,
transactionRequestAttributeId: Option[String],
Expand All @@ -3207,12 +3229,14 @@ object NewStyle extends MdcLoggable{

def createTransactionRequestAttributes(bankId: BankId,
transactionRequestId: TransactionRequestId,
transactionRequestAttributes: List[TransactionRequestAttributeTrait],
transactionRequestAttributes: List[TransactionRequestAttributeJsonV400],
isPersonal: Boolean,
callContext: Option[CallContext]): OBPReturnType[List[TransactionRequestAttributeTrait]] = {
Connector.connector.vend.createTransactionRequestAttributes(
bankId: BankId,
transactionRequestId: TransactionRequestId,
transactionRequestAttributes: List[TransactionRequestAttributeTrait],
transactionRequestAttributes: List[TransactionRequestAttributeJsonV400],
isPersonal: Boolean,
callContext: Option[CallContext]
) map {
i => (connectorEmptyResponse(i._1, callContext), i._2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ case class TransactionRequestBodyCounterpartyJSON(
value: AmountOfMoneyJsonV121,
description: String,
charge_policy: String,
future_date: Option[String] = None
future_date: Option[String] = None,
attributes: Option[List[TransactionRequestAttributeJsonV400]]= None,
) extends TransactionRequestCommonBodyJSON

// the data from endpoint, extract as valid JSON
Expand Down
Loading

0 comments on commit adfb813

Please sign in to comment.