Merge pull request #2551 from hongwei1/develop

Feature/Added OBPv510 revokeMyConsent
This commit is contained in:
Simon Redfern 2025-05-28 16:23:56 +02:00 committed by GitHub
commit 7f5c7307d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 126 additions and 34 deletions

View File

@ -4748,13 +4748,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
val user = AuthUser.getCurrentUser
val result = tryo {
val headers: List[HTTPParam] = addlParams.get(RequestHeader.`Consent-Id`)
.map(consentId => List(HTTPParam(RequestHeader.`Consent-Id`, List(consentId))))
.getOrElse(Nil)
endpoint(newRequest)(CallContext(
user = user,
requestHeaders = headers))
endpoint(newRequest)(CallContext(user = user))
}
val func: ((=> LiftResponse) => Unit) => Unit = result match {

View File

@ -28,7 +28,7 @@ package code.api.v4_0_0
import code.api.OBPRestHelper
import code.api.util.APIUtil.{OBPEndpoint, getAllowedEndpoints}
import code.api.util.{APIUtil, VersionedOBPApis}
import code.api.util.VersionedOBPApis
import code.api.v1_3_0.APIMethods130
import code.api.v1_4_0.APIMethods140
import code.api.v2_0_0.APIMethods200
@ -39,7 +39,7 @@ import code.api.v3_0_0.custom.CustomAPIMethods300
import code.api.v3_1_0.{APIMethods310, OBPAPI3_1_0}
import code.util.Helper.MdcLoggable
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.util.{ApiVersion,ApiVersionStatus}
import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus}
import net.liftweb.common.{Box, Full}
import net.liftweb.http.{LiftResponse, PlainTextResponse}
import org.apache.http.HttpStatus
@ -63,6 +63,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w
nameOf(Implementations1_2_1.addPermissionForUserForBankAccountForOneView) ::
nameOf(Implementations1_2_1.removePermissionForUserForBankAccountForOneView) ::
nameOf(Implementations3_1_0.createAccount) ::
nameOf(Implementations3_1_0.revokeConsent) :://this endpoint is not restful, we do not support it in V510.
Nil
// if old version ResourceDoc objects have the same name endpoint with new version, omit old version ResourceDoc.

View File

@ -14,17 +14,17 @@ import code.api.util.NewStyle.HttpCode
import code.api.util.NewStyle.function.extractQueryParams
import code.api.util.X509.{getCommonName, getEmailAddress, getOrganization}
import code.api.util._
import code.api.util.newstyle.{BalanceNewStyle, RegulatedEntityAttributeNewStyle}
import code.api.util.newstyle.Consumer.createConsumerNewStyle
import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewStyle, deleteRegulatedEntityNewStyle, getRegulatedEntitiesNewStyle, getRegulatedEntityByEntityIdNewStyle}
import code.api.util.newstyle.{BalanceNewStyle, RegulatedEntityAttributeNewStyle}
import code.api.v2_0_0.AccountsHelper.{accountTypeFilterText, getFilteredCoreAccounts}
import code.api.v2_1_0.{ConsumerRedirectUrlJSON, JSONFactory210}
import code.api.v3_0_0.JSONFactory300
import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson
import code.api.v3_1_0.{ConsentChallengeJsonV310, ConsentJsonV310, PostConsentBodyCommonJson, PostConsentEmailJsonV310, PostConsentPhoneJsonV310}
import code.api.v3_1_0.JSONFactory310.{createBadLoginStatusJson, createConsumerJSON, createRefreshUserJson}
import code.api.v3_1_0.JSONFactory310.{createBadLoginStatusJson, createConsumerJSON}
import code.api.v3_1_0._
import code.api.v4_0_0.JSONFactory400.{createAccountBalancesJson, createBalancesJson, createNewCoreBankAccountJson}
import code.api.v4_0_0.{JSONFactory400, PostAccountAccessJsonV400, PostApiCollectionJson400, PutConsentStatusJsonV400, PutConsentUserJsonV400, RevokedJsonV400}
import code.api.v4_0_0._
import code.api.v5_0_0.JSONFactory500
import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson}
import code.atmattribute.AtmAttribute
@ -34,9 +34,8 @@ import code.consumer.Consumers
import code.entitlement.Entitlement
import code.loginattempts.LoginAttempt
import code.metrics.APIMetrics
import code.metrics.MappedMetric.userId
import code.model.{AppType, Consumer}
import code.model.dataAccess.{AuthUser, MappedBankAccount}
import code.model.{AppType, Consumer}
import code.regulatedentities.MappedRegulatedEntityProvider
import code.userlocks.UserLocksProvider
import code.users.Users
@ -47,7 +46,7 @@ import code.views.system.{AccountAccess, ViewDefinition}
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.{AtmAttributeType, ConsentType, RegulatedEntityAttributeType, StrongCustomerAuthentication, TransactionRequestStatus, UserAttributeType}
import com.openbankproject.commons.model.enums.{TransactionRequestStatus, _}
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
import net.liftweb.common.Full
import net.liftweb.http.rest.RestHelper
@ -57,7 +56,6 @@ import net.liftweb.mapper.By
import net.liftweb.util.Helpers.tryo
import net.liftweb.util.{Helpers, Props, StringHelpers}
import java.text.SimpleDateFormat
import java.time.{LocalDate, ZoneId}
import java.util.Date
import scala.collection.immutable.{List, Nil}
@ -1882,7 +1880,56 @@ trait APIMethods510 {
}
}
resourceDocs += ResourceDoc(
revokeMyConsent,
implementedInApiVersion,
nameOf(revokeMyConsent),
"Delete",
"/my/consents/CONSENT_ID",
"Revoke My Consent",
s"""
|Revoke Consent for current user specified by CONSENT_ID
|
|There are a few reasons you might need to revoke an applications access to a users account:
| - The user explicitly wishes to revoke the applications access
| - You as the service provider have determined an application is compromised or malicious, and want to disable it
| - etc.
|
|Please note that this endpoint only supports the case:: "The user explicitly wishes to revoke the applications access"
|
|OBP as a resource server stores access tokens in a database, then it is relatively easy to revoke some token that belongs to a particular user.
|The status of the token is changed to "REVOKED" so the next time the revoked client makes a request, their token will fail to validate.
|
|${userAuthenticationMessage(true)}
|
""".stripMargin,
EmptyBody,
revokedConsentJsonV310,
List(
$UserNotLoggedIn,
UnknownError
),
List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2))
lazy val revokeMyConsent: OBPEndpoint = {
case "my" :: "consents" :: consentId :: Nil JsonDelete _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map {
unboxFullOrFail(_, callContext, ConsentNotFound, 404)
}
_ <- Helper.booleanToFuture(failMsg = ConsentNotFound, cc=callContext) {
consent.mUserId == user.userId
}
consent <- Future(Consents.consentProvider.vend.revoke(consentId)) map {
i => connectorEmptyResponse(i, callContext)
}
} yield {
(ConsentJsonV310(consent.consentId, consent.jsonWebToken, consent.status), HttpCode.`200`(callContext))
}
}
}
val generalObpConsentText: String =
s"""
|

View File

@ -26,7 +26,6 @@ TESOBE (http://www.tesobe.com/)
*/
package code.snippet
import code.api.RequestHeader
import code.api.util.APIUtil.callEndpoint
import code.api.util.CustomJsonFormats
import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0
@ -123,9 +122,8 @@ class ConsentScreen extends MdcLoggable {
<div class={"alert " + alertClass} role="alert">{msg}</div>
}
private def selfRevokeConsent(consentId: String): Either[(String, Int), String] = {
val addlParams = Map(RequestHeader.`Consent-Id` -> consentId)
callEndpoint(Implementations5_1_0.selfRevokeConsent, List("my", "consent", "current"), DeleteRequest, addlParams = addlParams)
private def callRevokeMyConsent(consentId: String): Either[(String, Int), String] = {
callEndpoint(Implementations5_1_0.revokeMyConsent, List("my", "consents", consentId), DeleteRequest)
}
private def refreshTable(): JsCmd = {
@ -159,7 +157,7 @@ class ConsentScreen extends MdcLoggable {
<td>
{
SHtml.ajaxButton("Revoke", () => {
val result = selfRevokeConsent(consent.consent_id)
val result = callRevokeMyConsent(consent.consent_id)
val message = result match {
case Left((msg, _)) => ShowMessage(msg, isError = true)
case Right(_) => ShowMessage(s"Consent (reference_id ${consent.consent_reference_id}) successfully revoked.", isError = false)

View File

@ -119,11 +119,6 @@ Berlin 13359, Germany
<lift:loc locid="api_explorer">API Explorer</lift:loc>
</a>
</li>
<li class="navitem">
<a class="navlink api-explorer-link" href="/consents">
<lift:loc locid="Consents">Consents</lift:loc>
</a>
</li>
<li data-lift="Nav.item?name=Consumer%20Registration&showEvenIfRestricted=true" class="navitem">
<a id ="get-api-key-link" class="navlink" href="#">Link name. Has class "selected" if it's the current page.</a>
</li>

View File

@ -61,6 +61,9 @@ Berlin 13359, Germany
<label for="user-info-user-id">User ID</label>
<input readonly type="text" id="user-info-user-id" class="form-control" aria-describedby="consumer-registration-app-name-error">
</div>
<div>
<button type="button" class="btn btn-danger" onclick="location.href='/consents'">My Consents</button>
</div>
</div>
</div>

View File

@ -31,7 +31,7 @@ import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole._
import code.api.util.Consent
import code.api.util.ErrorMessages._
import code.api.v3_1_0.{PostConsentChallengeJsonV310, PostConsentEntitlementJsonV310}
import code.api.v3_1_0.{ConsentJsonV310, PostConsentChallengeJsonV310, PostConsentEntitlementJsonV310}
import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0
import code.api.v4_0_0.{PutConsentStatusJsonV400, UsersJsonV400}
import code.api.v5_0_0.OBPAPI5_0_0.Implementations5_0_0
@ -71,6 +71,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
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))
object revokeMyConsent extends Tag(nameOf(Implementations5_1_0.revokeMyConsent))
lazy val entitlements = List(PostConsentEntitlementJsonV310("", CanGetAnyUser.toString()))
lazy val bankId = testBankId1.value
@ -99,6 +100,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
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
def revokeMyConsentUrl(consentId: String) = (v5_1_0_Request / "my" / "consents" / consentId ).DELETE
feature(s"test $ApiEndpoint6 version $VersionOfApi - Unauthorized access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint6, VersionOfApi) {
@ -119,7 +121,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
}
}
feature(s"test $ApiEndpoint8 version $VersionOfApi - Unautenticated access") {
feature(s"test $ApiEndpoint8 version $VersionOfApi - Unauthenticated access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint8, VersionOfApi) {
When(s"We make a request $ApiEndpoint8")
val response510 = makeGetRequest(getMyConsentAtBank("whatever"))
@ -128,7 +130,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $ApiEndpoint8 version $VersionOfApi - Autenticated access") {
feature(s"test $ApiEndpoint8 version $VersionOfApi - Authenticated access") {
scenario("We will call the endpoint with user credentials", ApiEndpoint8, VersionOfApi) {
When(s"We make a request $ApiEndpoint1")
val response510 = makeGetRequest(getMyConsentAtBank("whatever")<@(user1))
@ -137,7 +139,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
}
}
feature(s"test $getMyConsents version $VersionOfApi - Unautenticated access") {
feature(s"test $getMyConsents version $VersionOfApi - Unauthenticated access") {
scenario("We will call the endpoint without user credentials", getMyConsents, VersionOfApi) {
When(s"We make a request $getMyConsents")
val response510 = makeGetRequest(getMyConsent("whatever"))
@ -146,7 +148,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $getMyConsents version $VersionOfApi - Autenticated access") {
feature(s"test $getMyConsents version $VersionOfApi - Authenticated access") {
scenario("We will call the endpoint with user credentials", getMyConsents, VersionOfApi) {
When(s"We make a request $ApiEndpoint1")
val response510 = makeGetRequest(getMyConsent("whatever")<@(user1))
@ -156,7 +158,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
}
feature(s"test $ApiEndpoint9 version $VersionOfApi - Unautenticated access") {
feature(s"test $ApiEndpoint9 version $VersionOfApi - Unauthenticated access") {
scenario("We will call the endpoint without user credentials", ApiEndpoint9, VersionOfApi) {
When(s"We make a request $ApiEndpoint9")
val response510 = makeGetRequest(getConsentsAtBAnk("whatever"))
@ -165,7 +167,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $ApiEndpoint9 version $VersionOfApi - Autenticated access") {
feature(s"test $ApiEndpoint9 version $VersionOfApi - Authenticated access") {
scenario("We will call the endpoint with user credentials", ApiEndpoint9, VersionOfApi) {
When(s"We make a request $ApiEndpoint1")
val response510 = makeGetRequest(getConsentsAtBAnk("whatever") <@ (user1))
@ -213,6 +215,17 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $revokeMyConsent version $VersionOfApi- Unauthenticated access") {
scenario("We will call the endpoint with user credentials", revokeMyConsent, VersionOfApi) {
When(s"We make a request $revokeMyConsent")
val response510 = makeDeleteRequest(revokeMyConsentUrl("xxxx"))
Then("We should get a 401")
response510.code should equal(401)
response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
}
feature(s"test $UpdateConsentStatusByConsent version $VersionOfApi - Authenticated access") {
scenario("We will call the endpoint with user credentials", UpdateConsentStatusByConsent, VersionOfApi) {
When(s"We make a request $UpdateConsentStatusByConsent")
@ -262,6 +275,15 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
response510.body.extract[ErrorMessage].message should startWith(ConsentNotFound)
}
}
feature(s"test $revokeMyConsent version $VersionOfApi") {
scenario("We will call the endpoint with user credentials", revokeMyConsent, VersionOfApi) {
When(s"We make a request $revokeMyConsent")
val response510 = makeDeleteRequest(revokeMyConsentUrl("xxxx")<@(user1))
Then("We should get a 404")
response510.code should equal(404)
response510.body.extract[ErrorMessage].message should startWith(ConsentNotFound)
}
}
feature(s"Create/Use/Revoke Consent $VersionOfApi") {
scenario("We will call the Create, Get and Delete endpoints with user credentials ", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, ApiEndpoint5, ApiEndpoint6, ApiEndpoint7, VersionOfApi) {
@ -283,7 +305,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
Then("We grant the role and test it again")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString)
val createConsentByRequestResponse = makePostRequest(createConsentByConsentRequestIdEmail(consentRequestId), write(""))
Then("We should get a 200")
Then("We should get a 201")
createConsentByRequestResponse.code should equal(201)
val consentId = createConsentByRequestResponse.body.extract[ConsentJsonV500].consent_id
val consentJwt = createConsentByRequestResponse.body.extract[ConsentJsonV500].jwt
@ -353,6 +375,37 @@ class ConsentsTest extends V510ServerSetup with PropsReset{
// We cannot get all users anymore
makeGetRequest(requestGetUsers, List(consentIdRequestHeader)).code should equal(401)
{
When(s"We try $ApiEndpoint1 v5.0.0")
val createConsentResponse = makePostRequest(createConsentRequestUrl, write(postConsentRequestJsonV310))
Then("We should get a 201")
createConsentResponse.code should equal(201)
val createConsentRequestResponseJson = createConsentResponse.body.extract[ConsentRequestResponseJson]
val consentRequestId = createConsentRequestResponseJson.consent_request_id
When("We try to make the GET request v5.0.0")
Then("We grant the role and test it again")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString)
val createConsentByRequestResponse = makePostRequest(createConsentByConsentRequestIdEmail(consentRequestId), write(""))
Then("We should get a 201")
createConsentByRequestResponse.code should equal(201)
val consentId = createConsentByRequestResponse.body.extract[ConsentJsonV500].consent_id
When(s"We make a request $revokeMyConsent")
val response510 = makeDeleteRequest(revokeMyConsentUrl(consentId)<@(user1))
Then("We should get a 200")
response510.code should equal(200)
response510.body.extract[ConsentJsonV310].status shouldBe("REVOKED")
When("We try to make the GET request v5.0.0")
// We cannot get all users anymore
makeGetRequest(requestGetUsers, List(consentIdRequestHeader)).code should equal(401)
}
}
}