Skip to content

Commit

Permalink
Merge pull request #2470 from constantine2nd/develop
Browse files Browse the repository at this point in the history
oauth2.keycloak.client_ids
  • Loading branch information
simonredfern authored Dec 23, 2024
2 parents 40e06cd + 1dc4ee2 commit 82629d8
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 6 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 @@ -769,6 +769,8 @@ display_internal_errors=false
# oauth2.keycloak.well_known=http://localhost:7070/realms/master/.well-known/openid-configuration
# Used to sync IAM of OBP-API and IAM of Keycloak
# oauth2.keycloak.source_of_truth = false
# LIst of clients allowed to sync IAM of OBP-API and IAM of Keycloak
# oauth2.keycloak.client_ids = SOME_CLIENT_ID_1, SOME_CLIENT_ID_2
# ------------------------------------------------------------------------------ OAuth 2 ------

# -- PSU Authentication methods --------------------------------------------------------------
Expand Down
17 changes: 13 additions & 4 deletions obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -517,16 +517,25 @@ object OAuth2Login extends RestHelper with MdcLoggable {

private def addScopesToConsumer(token: String, consumerPrimaryKey: Long): Unit = {
val sourceOfTruth = APIUtil.getPropsAsBoolValue(nameOfProperty = "oauth2.keycloak.source_of_truth", defaultValue = false)
// Consumers allowed to use the source of truth feature
val consumerIds: List[String] =
APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.client_ids").toList
.flatMap(_.split(",").toList)
val consumerId = getClaim(name = "azp", idToken = token).getOrElse("")
if(sourceOfTruth) {
logger.debug("Extracting roles from Access Token")
import net.liftweb.json._
val jsonString = JwtUtil.getSignedPayloadAsJson(token)
val json = parse(jsonString.getOrElse(""))
val openBankRoles: List[String] = {
(json \ "resource_access" \ consumerId \ "roles").extract[List[String]]
.filter(role => tryo(ApiRole.valueOf(role)).isDefined) // Keep only the roles OBP-API can recognise
}
val openBankRoles: List[String] =
if(consumerIds.contains(consumerId)) {
// Sync Keycloak's roles
(json \ "resource_access" \ consumerId \ "roles").extract[List[String]]
.filter(role => tryo(ApiRole.valueOf(role)).isDefined) // Keep only the roles OBP-API can recognise
} else {
// Clean up roles assigned to "consumerId"
List()
}
val scopes = Scope.scope.vend.getScopesByConsumerId(consumerPrimaryKey.toString).getOrElse(Nil)
val databaseState = scopes.map(_.roleName)
// Already exist at DB
Expand Down
18 changes: 18 additions & 0 deletions obp-api/src/main/scala/code/api/util/ConsentUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -940,4 +940,22 @@ object Consent extends MdcLoggable {
consentsOfBank
}

def filterStrictlyByBank(consents: List[MappedConsent], bankId: String): 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 {
// There is a View related to Bank ID
case Full(consentJWT) if consentJWT.views.map(_.bank_id).contains(bankId) => true
// There is a Role related to Bank ID
case Full(consentJWT) if consentJWT.entitlements.map(_.bank_id).contains(bankId) => true
// Filter out Consent because there is no a View or a Role related to Bank ID
case _ => false
}
}
consentsOfBank
}

}
57 changes: 57 additions & 0 deletions obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,63 @@ trait APIMethods510 {
}
}

staticResourceDocs += ResourceDoc(
getConsents,
implementedInApiVersion,
nameOf(getConsents),
"GET",
"/management/consents",
"Get Consents",
s"""
|
|This endpoint gets the Consents.
|
|${authenticationRequiredMessage(true)}
|
|1 limit (for pagination: defaults to 50) eg:limit=200
|
|2 offset (for pagination: zero index, defaults to 0) eg: offset=10
|
|3 consumer_id (ignore if omitted)
|
|4 consent_id (ignore if omitted)
|
|5 user_id (ignore if omitted)
|
|6 status (ignore if omitted)
|
|7 bank_id (ignore if omitted)
|
|eg:/management/consents?consumer_id=78&limit=10&offset=10
|
""".stripMargin,
EmptyBody,
consentsJsonV510,
List(
$UserNotLoggedIn,
$BankNotFound,
UnknownError
),
List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2),
Some(List(canGetConsentsAtAnyBank)),
)

lazy val getConsents: OBPEndpoint = {
case "management" :: "consents" :: Nil JsonGet _ => {
cc =>
implicit val ec = EndpointContext(Some(cc))
for {
httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url)
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext)
consents <- Future {
Consents.consentProvider.vend.getConsents(obpQueryParams)
}
} yield {
(createConsentsJsonV510(consents), HttpCode.`200`(callContext))
}
}
}


staticResourceDocs += ResourceDoc(
getConsentByConsentId,
Expand Down
10 changes: 8 additions & 2 deletions obp-api/src/main/scala/code/consent/MappedConsent.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package code.consent

import java.util.Date
import code.api.util.{APIUtil, Consent, ErrorMessages, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPStatus, OBPUserId, SecureRandomUtil}
import code.api.util.{APIUtil, Consent, ErrorMessages, OBPBankId, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPStatus, OBPUserId, SecureRandomUtil}
import code.consent.ConsentStatus.ConsentStatus
import code.model.Consumer
import code.util.MappedUUID
Expand Down Expand Up @@ -85,7 +85,13 @@ object MappedConsentProvider extends ConsentProvider {

override def getConsents(queryParams: List[OBPQueryParam]): List[MappedConsent] = {
val optionalParams = getQueryParams(queryParams)
MappedConsent.findAll(optionalParams: _*)
val consents = MappedConsent.findAll(optionalParams: _*)
val bankId: Option[String] = queryParams.collectFirst { case OBPBankId(value) => value }
if(bankId.isDefined) {
Consent.filterStrictlyByBank(consents, bankId.get)
} else {
consents
}
}
override def createObpConsent(user: User, challengeAnswer: String, consentRequestId:Option[String], consumer: Option[Consumer]): Box[MappedConsent] = {
tryo {
Expand Down
30 changes: 30 additions & 0 deletions obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
object ApiEndpoint7 extends Tag(nameOf(Implementations5_1_0.getConsentByConsentId))
object ApiEndpoint8 extends Tag(nameOf(Implementations5_1_0.getMyConsents))
object ApiEndpoint9 extends Tag(nameOf(Implementations5_1_0.getConsentsAtBank))
object GetConsents extends Tag(nameOf(Implementations5_1_0.getConsents))
object UpdateConsentStatusByConsent extends Tag(nameOf(Implementations5_1_0.updateConsentStatusByConsent))
object UpdateConsentAccountAccessByConsentId extends Tag(nameOf(Implementations5_1_0.updateConsentAccountAccessByConsentId))

Expand All @@ -93,6 +94,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
def revokeConsentUrl(consentId: String) = (v5_1_0_Request / "banks" / bankId / "consents" / consentId).DELETE
def getMyConsents(consentId: String) = (v5_1_0_Request / "banks" / bankId / "my" / "consents").GET
def getConsentsAtBAnk(consentId: String) = (v5_1_0_Request / "management"/ "consents" / "banks" / bankId).GET
def getConsents(consentId: String) = (v5_1_0_Request / "management"/ "consents").GET
def updateConsentStatusByConsent(consentId: String) = (v5_1_0_Request / "management" / "banks" / bankId / "consents" / consentId).PUT
def updateConsentPayloadByConsent(consentId: String) = (v5_1_0_Request / "management" / "banks" / bankId / "consents" / consentId / "account-access").PUT

Expand Down Expand Up @@ -153,6 +155,34 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
}
}

feature(s"test $GetConsents version $VersionOfApi - Unauthenticated access") {
scenario("We will call the endpoint without user credentials", GetConsents, VersionOfApi) {
When(s"We make a request $GetConsents")
val response510 = makeGetRequest(getConsents("whatever"))
Then("We should get a 401")
response510.code should equal(401)
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $GetConsents version $VersionOfApi - Authenticated access") {
scenario("We will call the endpoint with user credentials", GetConsents, VersionOfApi) {
When(s"We make a request $ApiEndpoint1")
val response510 = makeGetRequest(getConsents("whatever") <@ (user1))
Then("We should get a 403")
response510.code should equal(403)
response510.body.extract[ErrorMessage].message contains (UserHasMissingRoles + s"$CanGetConsentsAtAnyBank") should be(true)
}
}
feature(s"test $GetConsents version $VersionOfApi - Authenticated access with proper entitlement") {
scenario("We will call the endpoint with user credentials", GetConsents, VersionOfApi) {
When(s"We make a request $ApiEndpoint1")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetConsentsAtAnyBank.toString)
val response510 = makeGetRequest(getConsents("whatever") <@ (user1))
Then("We should get a 200")
response510.code should equal(200)
}
}


feature(s"test $UpdateConsentStatusByConsent version $VersionOfApi - Unauthenticated access") {
scenario("We will call the endpoint without user credentials", UpdateConsentStatusByConsent, VersionOfApi) {
Expand Down

0 comments on commit 82629d8

Please sign in to comment.