Skip to content

Commit 72f261b

Browse files
authored
Merge pull request #2725 from hongwei1/develop-Simon
test/fixed failed tests
2 parents 381c844 + d47777d commit 72f261b

41 files changed

Lines changed: 2697 additions & 112 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Project Instructions
2+
3+
## Working Style
4+
- Never blame pre-existing issues or other commits. No excuses, no finger-pointing — diagnose and resolve.

obp-api/src/main/scala/bootstrap/liftweb/Boot.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,9 @@ object ToSchemify {
10141014
MappedRegulatedEntity,
10151015
AtmAttribute,
10161016
AbacRule,
1017+
code.mandate.Mandate,
1018+
code.mandate.MandateProvision,
1019+
code.mandate.SignatoryPanel,
10171020
MappedBank,
10181021
MappedBankAccount,
10191022
BankAccountRouting,

obp-api/src/main/scala/code/api/constant/constant.scala

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,8 @@ object Constant extends MdcLoggable {
379379
final val CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY = "can_add_transaction_request_to_beneficiary"
380380
final val CAN_GRANT_ACCESS_TO_VIEWS = "can_grant_access_to_views"
381381
final val CAN_REVOKE_ACCESS_TO_VIEWS = "can_revoke_access_to_views"
382+
final val CAN_BYPASS_MAKER_CHECKER_SEPARATION = "can_bypass_maker_checker_separation"
383+
final val CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE = "can_answer_transaction_request_challenge"
382384

383385
final val SYSTEM_OWNER_VIEW_PERMISSION_ADMIN = List(
384386
CAN_SEE_AVAILABLE_VIEWS_FOR_BANK_ACCOUNT,
@@ -391,7 +393,9 @@ object Constant extends MdcLoggable {
391393
CAN_ADD_TRANSACTION_REQUEST_TO_ANY_ACCOUNT,
392394
CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY,
393395
CAN_GRANT_ACCESS_TO_VIEWS,
394-
CAN_REVOKE_ACCESS_TO_VIEWS
396+
CAN_REVOKE_ACCESS_TO_VIEWS,
397+
CAN_BYPASS_MAKER_CHECKER_SEPARATION,
398+
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE
395399
)
396400

397401
final val SYSTEM_MANAGER_VIEW_PERMISSION = List(
@@ -400,12 +404,16 @@ object Constant extends MdcLoggable {
400404
CAN_CREATE_CUSTOM_VIEW,
401405
CAN_DELETE_CUSTOM_VIEW,
402406
CAN_UPDATE_CUSTOM_VIEW,
403-
CAN_GET_CUSTOM_VIEW
407+
CAN_GET_CUSTOM_VIEW,
408+
CAN_BYPASS_MAKER_CHECKER_SEPARATION,
409+
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE
404410
)
405411

406412
final val SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_PERMISSION = List(
407413
CAN_ADD_TRANSACTION_REQUEST_TO_ANY_ACCOUNT,
408-
CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY
414+
CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY,
415+
CAN_BYPASS_MAKER_CHECKER_SEPARATION,
416+
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE
409417
)
410418

411419
final val SYSTEM_PUBLIC_VIEW_PERMISSION = List(
@@ -563,7 +571,9 @@ object Constant extends MdcLoggable {
563571
CAN_SEE_OTHER_ACCOUNT_ROUTING_SCHEME,
564572
CAN_SEE_OTHER_ACCOUNT_ROUTING_ADDRESS,
565573
CAN_SEE_TRANSACTION_STATUS,
566-
CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT
574+
CAN_ADD_TRANSACTION_REQUEST_TO_OWN_ACCOUNT,
575+
CAN_BYPASS_MAKER_CHECKER_SEPARATION,
576+
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE
567577
)
568578

569579
final val ALL_VIEW_PERMISSION_NAMES = List(
@@ -660,6 +670,8 @@ object Constant extends MdcLoggable {
660670
CAN_ADD_TRANSACTION_REQUEST_TO_BENEFICIARY,
661671
CAN_GRANT_ACCESS_TO_VIEWS,
662672
CAN_REVOKE_ACCESS_TO_VIEWS,
673+
CAN_BYPASS_MAKER_CHECKER_SEPARATION,
674+
CAN_ANSWER_TRANSACTION_REQUEST_CHALLENGE,
663675
)
664676

665677

obp-api/src/main/scala/code/api/util/ApiRole.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,36 @@ object ApiRole extends MdcLoggable{
774774
case class CanExecuteAbacRule(requiresBankId: Boolean = false) extends ApiRole
775775
lazy val canExecuteAbacRule = CanExecuteAbacRule()
776776

777+
// Mandate roles (bank-level)
778+
case class CanCreateMandate(requiresBankId: Boolean = true) extends ApiRole
779+
lazy val canCreateMandate = CanCreateMandate()
780+
case class CanGetMandate(requiresBankId: Boolean = true) extends ApiRole
781+
lazy val canGetMandate = CanGetMandate()
782+
case class CanUpdateMandate(requiresBankId: Boolean = true) extends ApiRole
783+
lazy val canUpdateMandate = CanUpdateMandate()
784+
case class CanDeleteMandate(requiresBankId: Boolean = true) extends ApiRole
785+
lazy val canDeleteMandate = CanDeleteMandate()
786+
787+
// Mandate Provision roles (bank-level)
788+
case class CanCreateMandateProvision(requiresBankId: Boolean = true) extends ApiRole
789+
lazy val canCreateMandateProvision = CanCreateMandateProvision()
790+
case class CanGetMandateProvision(requiresBankId: Boolean = true) extends ApiRole
791+
lazy val canGetMandateProvision = CanGetMandateProvision()
792+
case class CanUpdateMandateProvision(requiresBankId: Boolean = true) extends ApiRole
793+
lazy val canUpdateMandateProvision = CanUpdateMandateProvision()
794+
case class CanDeleteMandateProvision(requiresBankId: Boolean = true) extends ApiRole
795+
lazy val canDeleteMandateProvision = CanDeleteMandateProvision()
796+
797+
// Signatory Panel roles (bank-level)
798+
case class CanCreateSignatoryPanel(requiresBankId: Boolean = true) extends ApiRole
799+
lazy val canCreateSignatoryPanel = CanCreateSignatoryPanel()
800+
case class CanGetSignatoryPanel(requiresBankId: Boolean = true) extends ApiRole
801+
lazy val canGetSignatoryPanel = CanGetSignatoryPanel()
802+
case class CanUpdateSignatoryPanel(requiresBankId: Boolean = true) extends ApiRole
803+
lazy val canUpdateSignatoryPanel = CanUpdateSignatoryPanel()
804+
case class CanDeleteSignatoryPanel(requiresBankId: Boolean = true) extends ApiRole
805+
lazy val canDeleteSignatoryPanel = CanDeleteSignatoryPanel()
806+
777807
case class CanGetSystemLevelDynamicEntities(requiresBankId: Boolean = false) extends ApiRole
778808
lazy val canGetSystemLevelDynamicEntities = CanGetSystemLevelDynamicEntities()
779809

@@ -783,6 +813,9 @@ object ApiRole extends MdcLoggable{
783813
case class CanCreateBankLevelDynamicEntity(requiresBankId: Boolean = true) extends ApiRole
784814
lazy val canCreateBankLevelDynamicEntity = CanCreateBankLevelDynamicEntity()
785815

816+
case class CanCreateAnyBankLevelDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
817+
lazy val canCreateAnyBankLevelDynamicEntity = CanCreateAnyBankLevelDynamicEntity()
818+
786819
case class CanUpdateSystemLevelDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
787820
lazy val canUpdateSystemDynamicEntity = CanUpdateSystemLevelDynamicEntity()
788821

@@ -807,6 +840,9 @@ object ApiRole extends MdcLoggable{
807840
case class CanGetBankLevelDynamicEntities(requiresBankId: Boolean = true) extends ApiRole
808841
lazy val canGetBankLevelDynamicEntities = CanGetBankLevelDynamicEntities()
809842

843+
case class CanGetAnyBankLevelDynamicEntities(requiresBankId: Boolean = false) extends ApiRole
844+
lazy val canGetAnyBankLevelDynamicEntities = CanGetAnyBankLevelDynamicEntities()
845+
810846
case class CanGetDynamicEntityDiagnostics(requiresBankId: Boolean = false) extends ApiRole
811847
lazy val canGetDynamicEntityDiagnostics = CanGetDynamicEntityDiagnostics()
812848

obp-api/src/main/scala/code/api/util/ApiTag.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ object ApiTag {
4545
val apiTagEntitlement = ResourceDocTag("Entitlement")
4646
val apiTagRole = ResourceDocTag("Role")
4747
val apiTagABAC = ResourceDocTag("ABAC")
48+
val apiTagMandate = ResourceDocTag("Mandate")
4849
val apiTagScope = ResourceDocTag("Scope")
4950
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
5051
val apiTagCounterparty = ResourceDocTag("Counterparty")

obp-api/src/main/scala/code/api/util/ErrorMessages.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ object ErrorMessages {
572572
val AccountAccessRequestCannotBeCreated = "OBP-30277: Account Access Request could not be created."
573573
val AccountAccessRequestStatusNotInitiated = "OBP-30278: Account Access Request status is not INITIATED. Only INITIATED requests can be approved or rejected."
574574
val MakerCheckerSameUser = "OBP-30279: The checker (approver/rejecter) cannot be the same user as the maker (requestor). Maker/Checker separation is required."
575+
val MakerCheckerUnknownMaker = "OBP-30283: Maker/Checker separation is required for this view but the Transaction Request has no user_id (maker) recorded. Cannot verify separation."
575576
val BusinessJustificationRequired = "OBP-30280: Business justification is required."
576577
val CheckerCommentRequiredForRejection = "OBP-30281: A comment is required when rejecting an Account Access Request."
577578
val AccountAccessRequestCannotBeUpdated = "OBP-30282: Account Access Request could not be updated."

obp-api/src/main/scala/code/api/util/Glossary.scala

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2956,7 +2956,7 @@ object Glossary extends MdcLoggable {
29562956
|Berlin Group consents with status "received" that remain unfinished (e.g. the PSU never completed the SCA flow) beyond a configured time threshold are automatically rejected.
29572957
|
29582958
|* `berlin_group_outdated_consents_time_in_seconds` - Time in seconds after which an unfinished consent is considered outdated. Default: **300** (5 minutes).
2959-
|* `berlin_group_outdated_consents_interval_in_seconds` - How often (in seconds) the scheduler checks for outdated consents. Must be set to a value greater than 0 to enable the task. **Not set by default** (task is disabled).
2959+
|* `berlin_group_outdated_consents_interval_in_seconds` - How often (in seconds) the scheduler checks for outdated consents. Default: **599**. Set to 0 to disable.
29602960
|
29612961
|Example:
29622962
|
@@ -2967,13 +2967,16 @@ object Glossary extends MdcLoggable {
29672967
|## Expired Consents
29682968
|
29692969
|Berlin Group consents with status "valid" whose `validUntil` date has passed are automatically transitioned to "expired" status.
2970+
|OBP consents with status "ACCEPTED" whose `validUntil` date has passed are automatically transitioned to "EXPIRED" status.
29702971
|
2971-
|* `berlin_group_expired_consents_interval_in_seconds` - How often (in seconds) the scheduler checks for expired consents. Must be set to a value greater than 0 to enable the task. **Not set by default** (task is disabled).
2972+
|* `berlin_group_expired_consents_interval_in_seconds` - How often (in seconds) the scheduler checks for expired Berlin Group consents. Default: **597**. Set to 0 to disable.
2973+
|* `obp_expired_consents_interval_in_seconds` - How often (in seconds) the scheduler checks for expired OBP consents. Default: **595**. Set to 0 to disable.
29722974
|
29732975
|Example:
29742976
|
29752977
| # Check for expired consents every 120 seconds
29762978
| berlin_group_expired_consents_interval_in_seconds = 120
2979+
| obp_expired_consents_interval_in_seconds = 120
29772980
|
29782981
""")
29792982

@@ -5251,6 +5254,72 @@ object Glossary extends MdcLoggable {
52515254
|
52525255
""")
52535256

5257+
glossaryItems += GlossaryItem(
5258+
title = "Mandates",
5259+
description =
5260+
s"""
5261+
|# Mandates
5262+
|
5263+
|## Overview
5264+
|
5265+
|A Mandate is a formal agreement between a corporate customer and a bank that defines who can operate an account, what they can do, and under what conditions.
5266+
|
5267+
|In OBP, a Mandate is an entity that ties together existing authorisation constructs (Views, ABAC Rules, Challenges) into a single, auditable policy document.
5268+
|
5269+
|## Structure
5270+
|
5271+
|A Mandate has three parts:
5272+
|
5273+
|### 1. Mandate
5274+
|
5275+
|The top-level container. It is linked to a bank account and a corporate customer, and holds the legal text, status (ACTIVE, SUSPENDED, EXPIRED, DRAFT), and validity period.
5276+
|
5277+
|### 2. Mandate Provisions
5278+
|
5279+
|Each provision maps a clause of the mandate to an OBP enforcement mechanism. Provision types:
5280+
|
5281+
|- **SIGNATORY_RULE** — defines who can sign and in what combination (e.g., "2 from Panel A" or "1 from Panel A and 1 from Panel B")
5282+
|- **VIEW_ASSIGNMENT** — links a Signatory Panel to a View, controlling what members of that panel can see and do
5283+
|- **ABAC_CONDITION** — links to an ABAC rule for attribute-based conditions (e.g., department matching, amount limits)
5284+
|- **RESTRICTION** — a negative rule that blocks certain operations (e.g., no international payments)
5285+
|- **NOTIFICATION** — triggers a notification rather than blocking (e.g., alert CFO for payments over a threshold)
5286+
|
5287+
|Provisions can specify conditions (e.g., amount thresholds, currency), link to a View, an ABAC Rule, and/or a Challenge type.
5288+
|
5289+
|### 3. Signatory Panels
5290+
|
5291+
|A Signatory Panel is a named set of users who are authorised to act under the mandate. For example:
5292+
|
5293+
|- Panel A: Directors (user-1, user-2, user-3)
5294+
|- Panel B: Finance team (user-4, user-5)
5295+
|
5296+
|Provisions reference panels by ID and specify how many signatories are required from each panel.
5297+
|
5298+
|## How it connects to existing OBP features
5299+
|
5300+
|- **Views** control what each panel member can see and do on the account (e.g., canSeeTransactionAmount, canAddTransactionRequestToBeneficiary)
5301+
|- **ABAC Rules** provide attribute-based conditions evaluated at runtime (e.g., user department must match account business unit)
5302+
|- **Challenges / Maker-Checker** enforce signatory requirements. A provision can require multiple challenges answered by different users from specified panels
5303+
|- **Corporate Customers** (CORPORATE / SUBSIDIARY types with parent-child hierarchy) represent the legal entities that mandates apply to
5304+
|
5305+
|## Example
5306+
|
5307+
|ACME Corp has a mandate on their operating account:
5308+
|
5309+
|1. Panel A (Directors): user-1, user-2, user-3
5310+
|2. Panel B (Finance): user-4, user-5
5311+
|3. Provision: payments < 5,000 EUR require 1 signature from Panel A
5312+
|4. Provision: payments 5,000-50,000 EUR require 2 signatures from Panel A
5313+
|5. Provision: payments > 50,000 EUR require 1 from Panel A and 1 from Panel B
5314+
|
5315+
|## API Endpoints
5316+
|
5317+
|Mandates, Provisions, and Signatory Panels each have CRUD endpoints under the Mandate tag.
5318+
|
5319+
|All endpoints require bank-level roles (e.g., CanCreateMandate, CanGetMandateProvision, CanUpdateSignatoryPanel).
5320+
|
5321+
""")
5322+
52545323
///////////////////////////////////////////////////////////////////
52555324
// NOTE! Some glossary items are generated in ExampleValue.scala
52565325
//////////////////////////////////////////////////////////////////

obp-api/src/main/scala/code/api/util/NewStyle.scala

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import code.users._
4141
import code.util.Helper
4242
import code.util.Helper.MdcLoggable
4343
import code.validation.{JsonSchemaValidationProvider, JsonValidation}
44+
import code.transactionChallenge.Challenges
45+
import code.transactionrequests.TransactionRequests
4446
import code.views.Views
47+
import code.views.system.ViewPermission
4548
import code.webhook.AccountWebhook
4649
import com.github.dwickern.macros.NameOf.nameOf
4750
import com.openbankproject.commons.dto.{CustomerAndAttribute, GetProductsParam, ProductCollectionItemsTree}
@@ -546,6 +549,73 @@ object NewStyle extends MdcLoggable{
546549
}
547550
}
548551

552+
def checkMakerCheckerForTransactionRequest(
553+
bankId: BankId,
554+
accountId: AccountId,
555+
viewId: ViewId,
556+
transactionRequestId: TransactionRequestId,
557+
challengeId: String,
558+
checkerUserId: String,
559+
callContext: Option[CallContext]
560+
): Future[Boolean] = {
561+
Future {
562+
// Check if the view has the can_bypass_maker_checker_separation permission
563+
val permissionName = Constant.CAN_BYPASS_MAKER_CHECKER_SEPARATION
564+
val hasPermission = Views.views.vend.customView(viewId, BankIdAccountId(bankId, accountId)) match {
565+
case Full(view) => ViewPermission.findViewPermission(view, permissionName).isDefined
566+
case _ => Views.views.vend.systemView(viewId) match {
567+
case Full(view) => ViewPermission.findViewPermission(view, permissionName).isDefined
568+
case _ => false
569+
}
570+
}
571+
572+
// If the view has can_bypass_maker_checker_separation, no check needed
573+
if (hasPermission) {
574+
Full(true)
575+
} else {
576+
// Maker-checker is required — get the TR
577+
val transactionRequest = TransactionRequests.transactionRequestProvider.vend
578+
.getTransactionRequest(transactionRequestId)
579+
580+
transactionRequest match {
581+
case Full(tr) =>
582+
tr.user_id match {
583+
case None | Some("") =>
584+
Failure(MakerCheckerUnknownMaker)
585+
case Some(makerUserId) if makerUserId == checkerUserId =>
586+
// Same user as maker — check if this is a multi-challenge scenario
587+
// where the user is answering their own assigned SCA challenge
588+
val challenges = Challenges.ChallengeProvider.vend
589+
.getChallengesByTransactionRequestId(transactionRequestId.value)
590+
challenges match {
591+
case Full(challengeList) if challengeList.size > 1 =>
592+
// Multiple challenges: allow if this specific challenge is assigned to the checker
593+
val isOwnChallenge = challengeList.exists(c =>
594+
c.challengeId == challengeId && c.expectedUserId == checkerUserId
595+
)
596+
if (isOwnChallenge) Full(true)
597+
else Failure(MakerCheckerSameUser)
598+
case _ =>
599+
// Single challenge or no challenges found: block same user
600+
Failure(MakerCheckerSameUser)
601+
}
602+
case _ =>
603+
tr.on_behalf_of_user_id match {
604+
case Some(onBehalfOfUserId) if onBehalfOfUserId.nonEmpty && onBehalfOfUserId == checkerUserId =>
605+
Failure(MakerCheckerSameUser)
606+
case _ =>
607+
Full(true)
608+
}
609+
}
610+
case _ =>
611+
Failure(s"$InvalidTransactionRequestId Current TransactionRequestId($transactionRequestId)")
612+
}
613+
}
614+
} map {
615+
unboxFullOrFail(_, callContext, "Maker/Checker check failed")
616+
}
617+
}
618+
549619
def getConsumerByConsumerId(consumerId: String, callContext: Option[CallContext]): Future[Consumer] = {
550620
Consumers.consumers.vend.getConsumerByConsumerIdFuture(consumerId) map {
551621
unboxFullOrFail(_, callContext, s"$ConsumerNotFoundByConsumerId Current ConsumerId is $consumerId", 404)
@@ -1269,7 +1339,7 @@ object NewStyle extends MdcLoggable{
12691339
}
12701340
}
12711341

1272-
def validateChallengeAnswer(challengeId: String, suppliedAnswer: String, suppliedAnswerType:SuppliedAnswerType.Value, callContext: Option[CallContext]): OBPReturnType[Boolean] =
1342+
def validateChallengeAnswer(challengeId: String, suppliedAnswer: String, suppliedAnswerType:SuppliedAnswerType.Value, callContext: Option[CallContext]): OBPReturnType[Boolean] =
12731343
Connector.connector.vend.validateChallengeAnswerV2(challengeId, suppliedAnswer, suppliedAnswerType, callContext) map { i =>
12741344
(unboxFullOrFail(i._1, callContext, s"${
12751345
InvalidChallengeAnswer
@@ -1278,6 +1348,30 @@ object NewStyle extends MdcLoggable{
12781348
}"), i._2)
12791349
}
12801350

1351+
/**
1352+
* Validate a challenge answer without checking the userId.
1353+
* Used when a checker (different user) answers a challenge that was assigned to the maker,
1354+
* in the single-challenge maker-checker scenario. The maker-checker check has already
1355+
* verified this user is allowed to answer.
1356+
*/
1357+
def validateChallengeAnswerWithoutUserIdCheck(
1358+
challengeId: String,
1359+
suppliedAnswer: String,
1360+
suppliedAnswerType: SuppliedAnswerType.Value,
1361+
callContext: Option[CallContext]
1362+
): OBPReturnType[Boolean] = {
1363+
Future {
1364+
val result = Challenges.ChallengeProvider.vend.validateChallenge(challengeId, suppliedAnswer, None)
1365+
(Full(result.isDefined), callContext)
1366+
} map { i =>
1367+
(unboxFullOrFail(i._1, callContext, s"${
1368+
InvalidChallengeAnswer
1369+
.replace("answer may be expired.", s"answer may be expired (${transactionRequestChallengeTtl} seconds).")
1370+
.replace("up your allowed attempts.", s"up your allowed attempts (${allowedAnswerTransactionRequestChallengeAttempts} times).")
1371+
}"), i._2)
1372+
}
1373+
}
1374+
12811375
def allChallengesSuccessfullyAnswered(
12821376
bankId: BankId,
12831377
accountId: AccountId,

0 commit comments

Comments
 (0)