Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

oauth2.keycloak.client_ids #2470

Merged
merged 2 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading