diff --git a/.gitignore b/.gitignore
index edee4261e..d990d9c46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,4 +40,5 @@ marketing_diagram_generation/outputs/*
.specstory
project/project
coursier
-metals.sbt
\ No newline at end of file
+metals.sbt
+obp-http4s-runner/src/main/resources/git.properties
diff --git a/README.md b/README.md
index a63ff2930..6d92e9c2b 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,17 @@ To compile and run Jetty, install Maven 3, create your configuration in `obp-api
mvn install -pl .,obp-commons && mvn jetty:run -pl obp-api
```
+### Running http4s server (obp-http4s-runner)
+
+To run the API using the http4s server (without Jetty), use the `obp-http4s-runner` module from the project root:
+
+```sh
+MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G" mvn -pl obp-http4s-runner -am clean package -DskipTests=true -Dmaven.test.skip=true && \
+java -jar obp-http4s-runner/target/obp-http4s-runner.jar
+```
+
+The http4s server binds to `http4s.host` / `http4s.port` as configured in your props file (defaults are `127.0.0.1` and `8181`).
+
### ZED IDE Setup
For ZED IDE users, we provide a complete development environment with Scala language server support:
diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index c11d23533..0ba0e454a 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -23,52 +23,6 @@
src/main/resources/web.xml
-
- http4s-jar
-
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
- 3.6.0
-
- false
- ${project.artifactId}-http4s
-
-
- bootstrap.http4s.Http4sServer
-
-
-
- jar-with-dependencies
-
-
-
- /
- true
- runtime
-
-
-
-
- ${project.build.outputDirectory}
- /
-
-
-
-
-
- http4s-fat-jar
- package
-
- single
-
-
-
-
-
-
-
@@ -83,7 +37,8 @@
com.tesobe
obp-commons
-
+
+
com.github.everit-org.json-schema
@@ -288,21 +244,21 @@
signpost-commonshttp4
1.2.1.2
-
+
- com.typesafe.akka
- akka-http-core_${scala.version}
- 10.1.6
+ org.apache.pekko
+ pekko-http-core_${scala.version}
+ 1.1.0
- com.typesafe.akka
- akka-actor_${scala.version}
- ${akka.version}
+ org.apache.pekko
+ pekko-actor_${scala.version}
+ ${pekko.version}
- com.typesafe.akka
- akka-remote_${scala.version}
- ${akka.version}
+ org.apache.pekko
+ pekko-remote_${scala.version}
+ ${pekko.version}
com.sksamuel.avro4s
@@ -316,8 +272,8 @@
com.twitter
- chill-akka_${scala.version}
- 0.9.1
+ chill_${scala.version}
+ 0.9.3
com.twitter
@@ -337,9 +293,9 @@
0.9.3
- com.typesafe.akka
- akka-slf4j_${scala.version}
- ${akka.version}
+ org.apache.pekko
+ pekko-slf4j_${scala.version}
+ ${pekko.version}
diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md b/obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md
new file mode 100644
index 000000000..d91148ab8
--- /dev/null
+++ b/obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md
@@ -0,0 +1,856 @@
+# ABAC Rule Object Properties Reference
+
+This document provides a comprehensive reference for all properties available on objects that can be used in ABAC (Attribute-Based Access Control) rules.
+
+## Overview
+
+When you write ABAC rules, you have access to eleven objects:
+
+1. **authenticatedUser** - The authenticated user making the API call (always available)
+2. **authenticatedUserAttributes** - Non-personal attributes for the authenticated user (always available)
+3. **authenticatedUserAuthContext** - Auth context for the authenticated user (always available)
+4. **onBehalfOfUserOpt** - Optional user for delegation scenarios
+5. **onBehalfOfUserAttributes** - Non-personal attributes for the onBehalfOf user (always available, may be empty)
+6. **onBehalfOfUserAuthContext** - Auth context for the onBehalfOf user (always available, may be empty)
+7. **user** - A user object (always available)
+8. **bankOpt** - Optional bank context
+9. **accountOpt** - Optional account context
+10. **transactionOpt** - Optional transaction context
+11. **customerOpt** - Optional customer context
+
+**Important: All objects are READ-ONLY.** You cannot modify user attributes, auth context, or any other objects within ABAC rules.
+
+## How to Use This Reference
+
+When writing ABAC rules, you can access properties using dot notation:
+
+```scala
+// Example: Check if authenticated user is admin
+authenticatedUser.emailAddress.endsWith("@admin.com")
+
+// Example: Check authenticated user attributes
+authenticatedUserAttributes.exists(attr => attr.name == "department" && attr.value == "finance")
+
+// Example: Check authenticated user auth context
+authenticatedUserAuthContext.exists(ctx => ctx.key == "session_id")
+
+// Example: Check if delegation is present
+onBehalfOfUserOpt.isDefined
+
+// Example: Check onBehalfOf user attributes
+onBehalfOfUserAttributes.exists(attr => attr.name == "role" && attr.value == "manager")
+
+// Example: Check onBehalfOf user auth context
+onBehalfOfUserAuthContext.exists(ctx => ctx.key == "device_id")
+
+// Example: Check if user has specific email
+user.emailAddress == "alice@example.com"
+
+// Example: Check if account balance is above 1000
+accountOpt.exists(account => account.balance.toDouble > 1000.0)
+
+// Example: Check if bank is in UK
+bankOpt.exists(bank => bank.bankId.value.startsWith("gh."))
+```
+
+---
+
+## 1. authenticatedUser (User)
+
+The authenticated user making the API call. This is always available (not optional).
+
+### Available Properties
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `userId` | `String` | Unique UUID for the user | `"f47ac10b-58cc-4372-a567-0e02b2c3d479"` |
+| `idGivenByProvider` | `String` | Same as username | `"alice@example.com"` |
+| `provider` | `String` | Authentication provider | `"obp"`, `"oauth"`, `"openid"` |
+| `emailAddress` | `String` | User's email address | `"alice@example.com"` |
+| `name` | `String` | User's full name | `"Alice Smith"` |
+| `createdByConsentId` | `Option[String]` | Consent ID if user created via consent | `Some("consent-123")` or `None` |
+| `createdByUserInvitationId` | `Option[String]` | User invitation ID if applicable | `Some("invite-456")` or `None` |
+| `isDeleted` | `Option[Boolean]` | Whether user is deleted | `Some(false)` or `None` |
+| `lastMarketingAgreementSignedDate` | `Option[Date]` | Last marketing agreement date | `Some(Date)` or `None` |
+| `lastUsedLocale` | `Option[String]` | Last used locale/language | `Some("en_GB")` or `None` |
+
+### Helper Methods
+
+| Method | Type | Description |
+|--------|------|-------------|
+| `isOriginalUser` | `Boolean` | True if user created by OBP (not via consent) |
+| `isConsentUser` | `Boolean` | True if user created via consent |
+
+### Example Rules Using authenticatedUser
+
+```scala
+// 1. Allow only admin users (by email suffix)
+authenticatedUser.emailAddress.endsWith("@admin.com")
+
+// 2. Allow specific user by ID
+authenticatedUser.userId == "f47ac10b-58cc-4372-a567-0e02b2c3d479"
+
+// 3. Allow only original users (not consent users)
+authenticatedUser.isOriginalUser
+
+// 4. Check if user has name
+authenticatedUser.name.nonEmpty
+
+// 5. Check authentication provider
+authenticatedUser.provider == "obp"
+
+// 6. Complex condition
+authenticatedUser.emailAddress.endsWith("@admin.com") ||
+authenticatedUser.name.contains("Manager")
+```
+
+---
+
+## 2. authenticatedUserAttributes (List[UserAttribute])
+
+Non-personal attributes for the authenticated user. This is always available (not optional), but may be an empty list.
+
+### UserAttribute Properties
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `userAttributeId` | `String` | Unique attribute ID | `"attr-123"` |
+| `userId` | `String` | User ID this attribute belongs to | `"user-456"` |
+| `name` | `String` | Attribute name | `"department"`, `"role"`, `"clearance_level"` |
+| `attributeType` | `UserAttributeType.Value` | Type of attribute | `UserAttributeType.STRING`, `UserAttributeType.INTEGER` |
+| `value` | `String` | Attribute value | `"finance"`, `"manager"`, `"5"` |
+| `insertDate` | `Date` | When attribute was created | `Date(...)` |
+| `isPersonal` | `Boolean` | Whether attribute is personal (always false here) | `false` |
+
+### Example Rules Using authenticatedUserAttributes
+
+```scala
+// 1. Check if user has a specific attribute
+authenticatedUserAttributes.exists(attr =>
+ attr.name == "department" && attr.value == "finance"
+)
+
+// 2. Check if user has clearance level >= 3
+authenticatedUserAttributes.exists(attr =>
+ attr.name == "clearance_level" &&
+ attr.value.toIntOption.exists(_ >= 3)
+)
+
+// 3. Check if user has any attributes
+authenticatedUserAttributes.nonEmpty
+
+// 4. Check multiple attributes (AND)
+val hasDepartment = authenticatedUserAttributes.exists(_.name == "department")
+val hasRole = authenticatedUserAttributes.exists(_.name == "role")
+hasDepartment && hasRole
+
+// 5. Get specific attribute value
+val departmentOpt = authenticatedUserAttributes.find(_.name == "department").map(_.value)
+departmentOpt.contains("finance")
+
+// 6. Check attribute with multiple possible values (OR)
+authenticatedUserAttributes.exists(attr =>
+ attr.name == "role" &&
+ List("admin", "manager", "supervisor").contains(attr.value)
+)
+
+// 7. Combine with user properties
+authenticatedUser.emailAddress.endsWith("@admin.com") ||
+authenticatedUserAttributes.exists(attr => attr.name == "admin_override" && attr.value == "true")
+```
+
+---
+
+## 3. authenticatedUserAuthContext (List[UserAuthContext])
+
+Authentication context for the authenticated user. This is always available (not optional), but may be an empty list.
+
+**READ-ONLY:** These values cannot be modified within ABAC rules.
+
+### UserAuthContext Properties
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `userAuthContextId` | `String` | Unique auth context ID | `"ctx-123"` |
+| `userId` | `String` | User ID this context belongs to | `"user-456"` |
+| `key` | `String` | Context key | `"session_id"`, `"ip_address"`, `"device_id"` |
+| `value` | `String` | Context value | `"sess-abc-123"`, `"192.168.1.1"`, `"device-xyz"` |
+| `timeStamp` | `Date` | When context was created | `Date(...)` |
+| `consumerId` | `String` | Consumer/app that created this context | `"consumer-789"` |
+
+### Example Rules Using authenticatedUserAuthContext
+
+```scala
+// 1. Check if user has a specific auth context
+authenticatedUserAuthContext.exists(ctx =>
+ ctx.key == "ip_address" && ctx.value.startsWith("192.168.")
+)
+
+// 2. Check if session exists
+authenticatedUserAuthContext.exists(ctx => ctx.key == "session_id")
+
+// 3. Check if auth context was recently created (within last hour)
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+
+authenticatedUserAuthContext.exists(ctx => {
+ val now = Instant.now()
+ val ctxInstant = ctx.timeStamp.toInstant
+ ChronoUnit.HOURS.between(ctxInstant, now) < 1
+})
+
+// 4. Check multiple context values (AND)
+val hasSession = authenticatedUserAuthContext.exists(_.key == "session_id")
+val hasDevice = authenticatedUserAuthContext.exists(_.key == "device_id")
+hasSession && hasDevice
+
+// 5. Get specific context value
+val ipAddressOpt = authenticatedUserAuthContext.find(_.key == "ip_address").map(_.value)
+ipAddressOpt.exists(ip => ip.startsWith("10.0."))
+
+// 6. Check consumer ID
+authenticatedUserAuthContext.exists(ctx =>
+ ctx.consumerId == "trusted-consumer-123"
+)
+
+// 7. Combine with user properties
+authenticatedUser.emailAddress.endsWith("@admin.com") &&
+authenticatedUserAuthContext.exists(_.key == "mfa_verified" && _.value == "true")
+```
+
+---
+
+## 4. onBehalfOfUserOpt (Option[User])
+
+Optional user for delegation scenarios. Present when someone acts on behalf of another user.
+
+This is an `Option[User]` - use `.exists()`, `.isDefined`, `.isEmpty`, or pattern matching.
+
+### Available Properties (when present)
+
+Same properties as `authenticatedUser` (see section 1 above).
+
+**Note:** When `onBehalfOfUserOpt` is present, the corresponding `onBehalfOfUserAttributes` and `onBehalfOfUserAuthContext` lists will contain data for that user.
+
+### Example Rules Using onBehalfOfUserOpt
+
+```scala
+// 1. Check if delegation is being used
+onBehalfOfUserOpt.isDefined
+
+// 2. Check if no delegation (direct access only)
+onBehalfOfUserOpt.isEmpty
+
+// 3. Check delegation user's email
+onBehalfOfUserOpt.exists(delegatedUser =>
+ delegatedUser.emailAddress.endsWith("@company.com")
+)
+
+// 4. Allow if authenticated user is customer service AND delegation is used
+val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com")
+val hasDelegation = onBehalfOfUserOpt.isDefined
+isCustomerService && hasDelegation
+
+// 5. Check both authenticated and delegation users
+onBehalfOfUserOpt match {
+ case Some(delegatedUser) =>
+ authenticatedUser.emailAddress.endsWith("@admin.com") &&
+ delegatedUser.emailAddress.nonEmpty
+ case None => true // No delegation, allow
+}
+```
+
+---
+
+## 5. onBehalfOfUserAttributes (List[UserAttribute])
+
+Non-personal attributes for the onBehalfOf user. This is always available (not optional), but will be an empty list if no delegation is happening.
+
+**READ-ONLY:** These values cannot be modified within ABAC rules.
+
+### UserAttribute Properties
+
+Same properties as `authenticatedUserAttributes` (see section 2 above).
+
+### Example Rules Using onBehalfOfUserAttributes
+
+```scala
+// 1. Check if onBehalfOf user has specific attribute
+onBehalfOfUserAttributes.exists(attr =>
+ attr.name == "department" && attr.value == "sales"
+)
+
+// 2. Check if onBehalfOf user has attributes (delegation with data)
+onBehalfOfUserAttributes.nonEmpty
+
+// 3. Verify delegation user has required role
+onBehalfOfUserOpt.isDefined &&
+onBehalfOfUserAttributes.exists(attr =>
+ attr.name == "role" && attr.value == "manager"
+)
+
+// 4. Compare authenticated and onBehalfOf user departments
+val authDept = authenticatedUserAttributes.find(_.name == "department").map(_.value)
+val onBehalfDept = onBehalfOfUserAttributes.find(_.name == "department").map(_.value)
+authDept == onBehalfDept
+
+// 5. Check clearance level for delegation
+onBehalfOfUserAttributes.exists(attr =>
+ attr.name == "clearance_level" &&
+ attr.value.toIntOption.exists(_ >= 2)
+)
+```
+
+---
+
+## 6. onBehalfOfUserAuthContext (List[UserAuthContext])
+
+Authentication context for the onBehalfOf user. This is always available (not optional), but will be an empty list if no delegation is happening.
+
+**READ-ONLY:** These values cannot be modified within ABAC rules.
+
+### UserAuthContext Properties
+
+Same properties as `authenticatedUserAuthContext` (see section 3 above).
+
+### Example Rules Using onBehalfOfUserAuthContext
+
+```scala
+// 1. Check if onBehalfOf user has active session
+onBehalfOfUserAuthContext.exists(ctx => ctx.key == "session_id")
+
+// 2. Verify onBehalfOf user IP is from internal network
+onBehalfOfUserAuthContext.exists(ctx =>
+ ctx.key == "ip_address" && ctx.value.startsWith("10.0.")
+)
+
+// 3. Check if both authenticated and onBehalfOf users have MFA
+val authHasMFA = authenticatedUserAuthContext.exists(_.key == "mfa_verified")
+val onBehalfHasMFA = onBehalfOfUserAuthContext.exists(_.key == "mfa_verified")
+authHasMFA && onBehalfHasMFA
+
+// 4. Verify delegation has auth context
+onBehalfOfUserOpt.isDefined && onBehalfOfUserAuthContext.nonEmpty
+
+// 5. Check consumer for delegation
+onBehalfOfUserAuthContext.exists(ctx =>
+ ctx.consumerId == "trusted-consumer-123"
+)
+```
+
+---
+
+## 7. user (User)
+
+A user object. This is always available (not optional).
+
+### Available Properties
+
+Same properties as `authenticatedUser` (see section 1 above).
+
+### Example Rules Using user
+
+```scala
+// 1. Check user email
+user.emailAddress == "alice@example.com"
+
+// 2. Check user by ID
+user.userId == "f47ac10b-58cc-4372-a567-0e02b2c3d479"
+
+// 3. Check user provider
+user.provider == "obp"
+
+// 4. Compare with authenticated user
+user.userId == authenticatedUser.userId
+
+// 5. Check if user owns account (if ownership data available)
+accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+```
+
+---
+
+## 8. bankOpt (Option[Bank])
+
+Optional bank context. Present when `bank_id` is provided in the API request.
+
+### Available Properties (when present)
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `bankId` | `BankId` | Unique bank identifier | `BankId("gh.29.uk")` |
+| `shortName` | `String` | Short name of bank | `"GH Bank"` |
+| `fullName` | `String` | Full legal name | `"Great Britain Bank Ltd"` |
+| `logoUrl` | `String` | URL to bank logo | `"https://example.com/logo.png"` |
+| `websiteUrl` | `String` | Bank website URL | `"https://www.ghbank.co.uk"` |
+| `bankRoutingScheme` | `String` | Routing scheme | `"SWIFT_BIC"`, `"UK.SORTCODE"` |
+| `bankRoutingAddress` | `String` | Routing address/code | `"GHBKGB2L"` |
+| `swiftBic` | `String` | SWIFT BIC code (deprecated) | `"GHBKGB2L"` |
+| `nationalIdentifier` | `String` | National identifier (deprecated) | `"123456"` |
+
+### Accessing BankId Value
+
+```scala
+// Get the string value from BankId
+bankOpt.exists(bank => bank.bankId.value == "gh.29.uk")
+```
+
+### Example Rules Using bankOpt
+
+```scala
+// 1. Allow only UK banks (by ID prefix)
+bankOpt.exists(bank =>
+ bank.bankId.value.startsWith("gh.") ||
+ bank.bankId.value.startsWith("uk.")
+)
+
+// 2. Allow specific bank
+bankOpt.exists(bank => bank.bankId.value == "gh.29.uk")
+
+// 3. Check bank name
+bankOpt.exists(bank => bank.shortName.contains("GH"))
+
+// 4. Check SWIFT BIC
+bankOpt.exists(bank => bank.swiftBic.startsWith("GHBK"))
+
+// 5. Allow if no bank context provided
+bankOpt.isEmpty
+
+// 6. Check website URL
+bankOpt.exists(bank => bank.websiteUrl.contains(".uk"))
+```
+
+---
+
+## 9. accountOpt (Option[BankAccount])
+
+Optional bank account context. Present when `account_id` is provided in the API request.
+
+### Available Properties (when present)
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `accountId` | `AccountId` | Unique account identifier | `AccountId("8ca8a7e4-6d02-48e3...")` |
+| `accountType` | `String` | Type of account | `"CURRENT"`, `"SAVINGS"`, `"330"` |
+| `balance` | `BigDecimal` | Current account balance | `1234.56` |
+| `currency` | `String` | Currency code (ISO 4217) | `"GBP"`, `"EUR"`, `"USD"` |
+| `name` | `String` | Account name | `"Main Checking Account"` |
+| `label` | `String` | Account label | `"Personal Account"` |
+| `number` | `String` | Account number | `"12345678"` |
+| `bankId` | `BankId` | Bank identifier | `BankId("gh.29.uk")` |
+| `lastUpdate` | `Date` | Last transaction refresh date | `Date(...)` |
+| `branchId` | `String` | Branch identifier | `"branch-123"` |
+| `accountRoutings` | `List[AccountRouting]` | Account routing information | `List(AccountRouting(...))` |
+| `accountRules` | `List[AccountRule]` | Account rules (optional) | `List(...)` |
+| `accountHolder` | `String` | Account holder name (deprecated) | `"Alice Smith"` |
+| `attributes` | `Option[List[Attribute]]` | Account attributes | `Some(List(...))` or `None` |
+
+### Important Notes
+
+- `balance` is a `BigDecimal` - convert to `Double` if needed: `account.balance.toDouble`
+- `accountId.value` gives the string value
+- `bankId.value` gives the bank ID string
+- Use `accountOpt.exists()` to safely check properties
+
+### Example Rules Using accountOpt
+
+```scala
+// 1. Check minimum balance
+accountOpt.exists(account => account.balance.toDouble >= 1000.0)
+
+// 2. Check account currency
+accountOpt.exists(account => account.currency == "GBP")
+
+// 3. Check account type
+accountOpt.exists(account => account.accountType == "CURRENT")
+
+// 4. Check account belongs to specific bank
+accountOpt.exists(account => account.bankId.value == "gh.29.uk")
+
+// 5. Check account number
+accountOpt.exists(account => account.number.startsWith("123"))
+
+// 6. Check if account has label
+accountOpt.exists(account => account.label.nonEmpty)
+
+// 7. Complex balance and currency check
+accountOpt.exists(account =>
+ account.balance.toDouble > 5000.0 &&
+ account.currency == "GBP"
+)
+
+// 8. Check account attributes (if available)
+accountOpt.exists(account =>
+ account.attributes.exists(attrs =>
+ attrs.exists(attr => attr.name == "accountStatus" && attr.value == "active")
+ )
+)
+```
+
+---
+
+## 10. transactionOpt (Option[Transaction])
+
+Optional transaction context. Present when `transaction_id` is provided in the API request.
+
+Uses the `TransactionCore` type.
+
+### Available Properties (when present)
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `id` | `TransactionId` | Unique transaction identifier | `TransactionId("trans-123")` |
+| `thisAccount` | `BankAccount` | The account this transaction belongs to | `BankAccount(...)` |
+| `otherAccount` | `CounterpartyCore` | The counterparty account | `CounterpartyCore(...)` |
+| `transactionType` | `String` | Type of transaction | `"DEBIT"`, `"CREDIT"` |
+| `amount` | `BigDecimal` | Transaction amount | `250.00` |
+| `currency` | `String` | Currency code | `"GBP"`, `"EUR"`, `"USD"` |
+| `description` | `Option[String]` | Transaction description | `Some("Payment to supplier")` or `None` |
+| `startDate` | `Date` | Transaction start date | `Date(...)` |
+| `finishDate` | `Date` | Transaction completion date | `Date(...)` |
+| `balance` | `BigDecimal` | Account balance after transaction | `1234.56` |
+
+### Example Rules Using transactionOpt
+
+```scala
+// 1. Allow transactions under a limit
+transactionOpt.exists(txn => txn.amount.toDouble < 10000.0)
+
+// 2. Check transaction type
+transactionOpt.exists(txn => txn.transactionType == "CREDIT")
+
+// 3. Check transaction currency
+transactionOpt.exists(txn => txn.currency == "GBP")
+
+// 4. Check transaction description
+transactionOpt.exists(txn =>
+ txn.description.exists(desc => desc.contains("salary"))
+)
+
+// 5. Check transaction belongs to account
+(transactionOpt, accountOpt) match {
+ case (Some(txn), Some(account)) =>
+ txn.thisAccount.accountId == account.accountId
+ case _ => false
+}
+
+// 6. Complex amount and type check
+transactionOpt.exists(txn =>
+ txn.amount.toDouble >= 100.0 &&
+ txn.amount.toDouble <= 5000.0 &&
+ txn.transactionType == "DEBIT"
+)
+
+// 7. Check recent transaction (within 30 days)
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+
+transactionOpt.exists(txn => {
+ val now = Instant.now()
+ val txnInstant = txn.finishDate.toInstant
+ ChronoUnit.DAYS.between(txnInstant, now) <= 30
+})
+```
+
+---
+
+## 11. customerOpt (Option[Customer])
+
+Optional customer context. Present when `customer_id` is provided in the API request.
+
+### Available Properties (when present)
+
+| Property | Type | Description | Example |
+|----------|------|-------------|---------|
+| `customerId` | `String` | Unique customer identifier (UUID) | `"cust-456-789"` |
+| `bankId` | `String` | Bank identifier | `"gh.29.uk"` |
+| `number` | `String` | Customer number (bank's identifier) | `"CUST123456"` |
+| `legalName` | `String` | Legal name of customer | `"Alice Jane Smith"` |
+| `mobileNumber` | `String` | Mobile phone number | `"+44 7700 900000"` |
+| `email` | `String` | Email address | `"alice@example.com"` |
+| `faceImage` | `CustomerFaceImageTrait` | Face image information | `CustomerFaceImage(...)` |
+| `dateOfBirth` | `Date` | Date of birth | `Date(1990, 1, 1)` |
+| `relationshipStatus` | `String` | Marital status | `"Single"`, `"Married"` |
+| `dependents` | `Integer` | Number of dependents | `2` |
+| `dobOfDependents` | `List[Date]` | Dates of birth of dependents | `List(Date(...))` |
+| `highestEducationAttained` | `String` | Education level | `"Bachelor's Degree"` |
+| `employmentStatus` | `String` | Employment status | `"Employed"`, `"Self-Employed"` |
+| `creditRating` | `CreditRatingTrait` | Credit rating information | `CreditRating(...)` |
+| `creditLimit` | `AmountOfMoneyTrait` | Credit limit | `AmountOfMoney(...)` |
+| `kycStatus` | `Boolean` | KYC verification status | `true` or `false` |
+| `lastOkDate` | `Date` | Last OK date | `Date(...)` |
+| `title` | `String` | Title | `"Mr"`, `"Ms"`, `"Dr"` |
+| `branchId` | `String` | Branch identifier | `"branch-123"` |
+| `nameSuffix` | `String` | Name suffix | `"Jr"`, `"III"` |
+
+### Example Rules Using customerOpt
+
+```scala
+// 1. Check KYC status
+customerOpt.exists(customer => customer.kycStatus == true)
+
+// 2. Check customer belongs to bank
+customerOpt.exists(customer => customer.bankId == "gh.29.uk")
+
+// 3. Check customer age (over 18)
+import java.time.LocalDate
+import java.time.Period
+import java.time.ZoneId
+
+customerOpt.exists(customer => {
+ val today = LocalDate.now()
+ val birthDate = LocalDate.ofInstant(customer.dateOfBirth.toInstant, ZoneId.systemDefault())
+ Period.between(birthDate, today).getYears >= 18
+})
+
+// 4. Check employment status
+customerOpt.exists(customer =>
+ customer.employmentStatus == "Employed" ||
+ customer.employmentStatus == "Self-Employed"
+)
+
+// 5. Check customer email matches user
+customerOpt.exists(customer => customer.email == user.emailAddress)
+
+// 6. Check number of dependents
+customerOpt.exists(customer => customer.dependents <= 3)
+
+// 7. Check education level
+customerOpt.exists(customer =>
+ customer.highestEducationAttained.contains("Degree")
+)
+
+// 8. Verify customer and account belong to same bank
+(customerOpt, accountOpt) match {
+ case (Some(customer), Some(account)) =>
+ customer.bankId == account.bankId.value
+ case _ => false
+}
+
+// 9. Check mobile number is provided
+customerOpt.exists(customer =>
+ customer.mobileNumber.nonEmpty && customer.mobileNumber != ""
+)
+```
+
+---
+
+## Complex Rule Examples
+
+### Example 1: Multi-Object Validation
+
+```scala
+// Allow if:
+// - Authenticated user is admin, OR
+// - Authenticated user has finance department attribute, OR
+// - User matches authenticated user AND account has sufficient balance
+
+val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com")
+val isFinance = authenticatedUserAttributes.exists(attr =>
+ attr.name == "department" && attr.value == "finance"
+)
+val isSelfAccess = user.userId == authenticatedUser.userId
+val hasBalance = accountOpt.exists(_.balance.toDouble > 1000.0)
+
+isAdmin || isFinance || (isSelfAccess && hasBalance)
+```
+
+### Example 2: Delegation Check with Attributes
+
+```scala
+// Allow if customer service is acting on behalf of someone with proper attributes
+val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com")
+val hasDelegation = onBehalfOfUserOpt.isDefined
+val onBehalfHasRole = onBehalfOfUserAttributes.exists(attr =>
+ attr.name == "role" && List("customer", "premium_customer").contains(attr.value)
+)
+val onBehalfHasSession = onBehalfOfUserAuthContext.exists(_.key == "session_id")
+
+isCustomerService && hasDelegation && onBehalfHasRole && onBehalfHasSession
+```
+
+### Example 3: Transaction Approval Based on Customer
+
+```scala
+// Allow transaction if:
+// - Customer is KYC verified AND
+// - Transaction is under limit AND
+// - Transaction currency matches account
+
+(customerOpt, transactionOpt, accountOpt) match {
+ case (Some(customer), Some(txn), Some(account)) =>
+ val isKycVerified = customer.kycStatus == true
+ val underLimit = txn.amount.toDouble < 10000.0
+ val correctCurrency = txn.currency == account.currency
+ isKycVerified && underLimit && correctCurrency
+ case _ => false
+}
+```
+
+### Example 4: Bank-Specific Rules
+
+```scala
+// Different rules for different banks
+bankOpt match {
+ case Some(bank) if bank.bankId.value.startsWith("gh.") =>
+ // UK bank rules - require higher balance
+ accountOpt.exists(_.balance.toDouble > 5000.0)
+ case Some(bank) if bank.bankId.value.startsWith("us.") =>
+ // US bank rules - require KYC
+ customerOpt.exists(_.kycStatus == true)
+ case Some(_) =>
+ // Other banks - basic check
+ user.emailAddress.nonEmpty
+ case None =>
+ // No bank context - deny
+ false
+}
+```
+
+---
+
+## Working with Optional Objects
+
+All objects except `authenticatedUser`, `authenticatedUserAttributes`, `authenticatedUserAuthContext`, `onBehalfOfUserAttributes`, `onBehalfOfUserAuthContext`, and `user` are optional. Here are patterns for working with them:
+
+### Pattern 1: exists()
+
+```scala
+// Check if bank exists and has a property
+bankOpt.exists(bank => bank.bankId.value == "gh.29.uk")
+```
+
+### Pattern 2: Pattern Matching
+
+```scala
+// Match on multiple objects simultaneously
+(bankOpt, accountOpt) match {
+ case (Some(bank), Some(account)) =>
+ bank.bankId == account.bankId
+ case _ => false
+}
+```
+
+### Pattern 3: isDefined / isEmpty
+
+```scala
+// Check if object is provided
+if (bankOpt.isDefined) {
+ val bank = bankOpt.get
+ bank.bankId.value == "gh.29.uk"
+} else {
+ false
+}
+```
+
+### Pattern 4: for Comprehension
+
+```scala
+// Chain multiple optional checks
+val result = for {
+ bank <- bankOpt
+ account <- accountOpt
+ if bank.bankId == account.bankId
+ if account.balance.toDouble > 1000.0
+} yield true
+
+result.getOrElse(false)
+```
+
+---
+
+## Common Patterns and Best Practices
+
+### 1. Type Conversions
+
+```scala
+// BigDecimal to Double
+account.balance.toDouble
+
+// Date comparisons
+txn.finishDate.before(new Date())
+txn.finishDate.after(new Date())
+
+// String to numeric
+account.number.toLong
+```
+
+### 2. String Operations
+
+```scala
+// Case-insensitive comparison
+user.emailAddress.toLowerCase == "alice@example.com"
+
+// Contains check
+bank.fullName.contains("Bank")
+
+// Starts with / Ends with
+user.emailAddress.endsWith("@admin.com")
+bank.bankId.value.startsWith("gh.")
+```
+
+### 3. List Operations
+
+```scala
+// Check if list is empty
+customer.dobOfDependents.isEmpty
+
+// Check list size
+customer.dobOfDependents.length > 0
+
+// Find in list
+account.accountRoutings.exists(routing => routing.scheme == "IBAN")
+```
+
+### 4. Safe Navigation
+
+```scala
+// Use getOrElse for defaults
+txn.description.getOrElse("No description")
+
+// Chain optional operations
+txn.description.getOrElse("No description").toLowerCase.contains("payment")
+```
+
+---
+
+## Import Statements Available
+
+These imports are automatically available in your ABAC rule code:
+
+```scala
+import com.openbankproject.commons.model._
+import code.model.dataAccess.ResourceUser
+import net.liftweb.common._
+```
+
+You can also use standard Scala/Java imports:
+
+```scala
+import java.time._
+import java.util.Date
+import scala.util._
+```
+
+---
+
+## Summary
+
+- **authenticatedUser**: Always available - the logged in user
+- **authenticatedUserAttributes**: Always available - list of non-personal attributes for authenticated user (may be empty)
+- **authenticatedUserAuthContext**: Always available - list of auth context for authenticated user (may be empty)
+- **onBehalfOfUserOpt**: Optional - present when delegation is used
+- **onBehalfOfUserAttributes**: Always available - list of non-personal attributes for onBehalfOf user (empty if no delegation)
+- **onBehalfOfUserAuthContext**: Always available - list of auth context for onBehalfOf user (empty if no delegation)
+- **user**: Always available - a user object
+- **bankOpt, accountOpt, transactionOpt, customerOpt**: Optional - use `.exists()` or pattern matching
+- **Type conversions**: Remember `.toDouble` for BigDecimal, `.value` for ID types
+- **Safe access**: Use `getOrElse()` for Option fields
+- **Build incrementally**: Break complex rules into named parts
+- **READ-ONLY**: All objects are read-only - you cannot modify them in rules
+
+---
+
+**Last Updated:** 2024
+**Related Documentation:** ABAC_SIMPLE_GUIDE.md, ABAC_REFACTORING.md, ABAC_TESTING_EXAMPLES.md
\ No newline at end of file
diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md b/obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md
new file mode 100644
index 000000000..73cb3f962
--- /dev/null
+++ b/obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md
@@ -0,0 +1,267 @@
+# ABAC Rule Parameters - Complete Reference
+
+This document lists all 16 parameters available in ABAC (Attribute-Based Access Control) rules.
+
+## Overview
+
+ABAC rules receive **18 parameters** that provide complete context for access control decisions.
+
+**All parameters are READ-ONLY** - you can only read and evaluate, never modify.
+
+## Complete Parameter List
+
+| # | Parameter | Type | Always Available? | Description |
+|---|-----------|------|-------------------|-------------|
+| 1 | `authenticatedUser` | `User` | ✅ Yes | The user who is logged in and making the API call |
+| 2 | `authenticatedUserAttributes` | `List[UserAttribute]` | ✅ Yes | Non-personal attributes for the authenticated user (may be empty) |
+| 3 | `authenticatedUserAuthContext` | `List[UserAuthContext]` | ✅ Yes | Auth context for the authenticated user (may be empty) |
+| 4 | `onBehalfOfUserOpt` | `Option[User]` | ❌ Optional | User being represented in delegation scenarios |
+| 5 | `onBehalfOfUserAttributes` | `List[UserAttribute]` | ✅ Yes | Non-personal attributes for onBehalfOf user (empty if no delegation) |
+| 6 | `onBehalfOfUserAuthContext` | `List[UserAuthContext]` | ✅ Yes | Auth context for onBehalfOf user (empty if no delegation) |
+| 7 | `userOpt` | `Option[User]` | ❌ Optional | A user object (when user_id is provided) |
+| 8 | `userAttributes` | `List[UserAttribute]` | ✅ Yes | Non-personal attributes for user (empty if no user) |
+| 9 | `bankOpt` | `Option[Bank]` | ❌ Optional | Bank object (when bank_id is provided) |
+| 10 | `bankAttributes` | `List[BankAttributeTrait]` | ✅ Yes | Attributes for bank (empty if no bank) |
+| 11 | `accountOpt` | `Option[BankAccount]` | ❌ Optional | Account object (when account_id is provided) |
+| 12 | `accountAttributes` | `List[AccountAttribute]` | ✅ Yes | Attributes for account (empty if no account) |
+| 13 | `transactionOpt` | `Option[Transaction]` | ❌ Optional | Transaction object (when transaction_id is provided) |
+| 14 | `transactionAttributes` | `List[TransactionAttribute]` | ✅ Yes | Attributes for transaction (empty if no transaction) |
+| 15 | `transactionRequestOpt` | `Option[TransactionRequest]` | ❌ Optional | Transaction request object (when transaction_request_id is provided) |
+| 16 | `transactionRequestAttributes` | `List[TransactionRequestAttributeTrait]` | ✅ Yes | Attributes for transaction request (empty if no transaction request) |
+| 17 | `customerOpt` | `Option[Customer]` | ❌ Optional | Customer object (when customer_id is provided) |
+| 18 | `customerAttributes` | `List[CustomerAttribute]` | ✅ Yes | Attributes for customer (empty if no customer) |
+
+## Function Signature
+
+```scala
+type AbacRuleFunction = (
+ User, // 1. authenticatedUser
+ List[UserAttribute], // 2. authenticatedUserAttributes
+ List[UserAuthContext], // 3. authenticatedUserAuthContext
+ Option[User], // 4. onBehalfOfUserOpt
+ List[UserAttribute], // 5. onBehalfOfUserAttributes
+ List[UserAuthContext], // 6. onBehalfOfUserAuthContext
+ Option[User], // 7. userOpt
+ List[UserAttribute], // 8. userAttributes
+ Option[Bank], // 9. bankOpt
+ List[BankAttributeTrait], // 10. bankAttributes
+ Option[BankAccount], // 11. accountOpt
+ List[AccountAttribute], // 12. accountAttributes
+ Option[Transaction], // 13. transactionOpt
+ List[TransactionAttribute], // 14. transactionAttributes
+ Option[TransactionRequest], // 15. transactionRequestOpt
+ List[TransactionRequestAttributeTrait], // 16. transactionRequestAttributes
+ Option[Customer], // 17. customerOpt
+ List[CustomerAttribute] // 18. customerAttributes
+) => Boolean
+```
+
+## Parameter Groups
+
+### Group 1: Authenticated User (Always Available)
+- `authenticatedUser` - The logged in user
+- `authenticatedUserAttributes` - Their non-personal attributes
+- `authenticatedUserAuthContext` - Their auth context (session, IP, etc.)
+
+### Group 2: OnBehalfOf User (Delegation)
+- `onBehalfOfUserOpt` - Optional delegated user
+- `onBehalfOfUserAttributes` - Their non-personal attributes (empty if no delegation)
+- `onBehalfOfUserAuthContext` - Their auth context (empty if no delegation)
+
+### Group 3: Target User (Optional)
+- `userOpt` - Optional user object
+- `userAttributes` - Their non-personal attributes (empty if no user)
+
+### Group 4: Bank (Optional)
+- `bankOpt` - Optional bank object
+- `bankAttributes` - Bank attributes (empty if no bank)
+
+### Group 5: Account (Optional)
+- `accountOpt` - Optional account object
+- `accountAttributes` - Account attributes (empty if no account)
+
+### Group 6: Transaction (Optional)
+- `transactionOpt` - Optional transaction object
+- `transactionAttributes` - Transaction attributes (empty if no transaction)
+
+### Group 7: Transaction Request (Optional)
+- `transactionRequestOpt` - Optional transaction request object
+- `transactionRequestAttributes` - Transaction request attributes (empty if no transaction request)
+
+### Group 8: Customer (Optional)
+- `customerOpt` - Optional customer object
+- `customerAttributes` - Customer attributes (empty if no customer)
+
+## Example Rules
+
+### Example 1: Check Authenticated User Attribute
+```scala
+authenticatedUserAttributes.exists(attr =>
+ attr.name == "department" && attr.value == "finance"
+)
+```
+
+### Example 2: Check Bank Attribute
+```scala
+bankAttributes.exists(attr =>
+ attr.name == "country" && attr.value == "UK"
+)
+```
+
+### Example 3: Check Account Attribute
+```scala
+accountAttributes.exists(attr =>
+ attr.name == "account_type" && attr.value == "premium"
+)
+```
+
+### Example 4: Check Transaction Attribute
+```scala
+transactionAttributes.exists(attr =>
+ attr.name == "risk_score" &&
+ attr.value.toIntOption.exists(_ < 5)
+)
+```
+
+### Example 5: Check Transaction Request Attribute
+```scala
+transactionRequestAttributes.exists(attr =>
+ attr.name == "approval_status" && attr.value == "pending"
+)
+```
+
+### Example 6: Check Customer Attribute
+```scala
+customerAttributes.exists(attr =>
+ attr.name == "kyc_status" && attr.value == "verified"
+)
+```
+
+### Example 7: Complex Multi-Attribute Rule
+```scala
+// Allow if:
+// - Authenticated user is in finance department
+// - Bank is in allowed countries
+// - Account is premium
+// - Transaction risk is low
+
+val authIsFinance = authenticatedUserAttributes.exists(attr =>
+ attr.name == "department" && attr.value == "finance"
+)
+
+val bankAllowed = bankAttributes.exists(attr =>
+ attr.name == "country" && List("UK", "US", "DE").contains(attr.value)
+)
+
+val accountPremium = accountAttributes.exists(attr =>
+ attr.name == "account_type" && attr.value == "premium"
+)
+
+val lowRisk = transactionAttributes.exists(attr =>
+ attr.name == "risk_score" && attr.value.toIntOption.exists(_ < 3)
+)
+
+authIsFinance && bankAllowed && accountPremium && lowRisk
+```
+
+### Example 8: Delegation with Attributes
+```scala
+// Allow customer service to help premium customers
+val isCustomerService = authenticatedUserAttributes.exists(attr =>
+ attr.name == "role" && attr.value == "customer_service"
+)
+
+val hasDelegation = onBehalfOfUserOpt.isDefined
+
+val customerIsPremium = onBehalfOfUserAttributes.exists(attr =>
+ attr.name == "customer_tier" && attr.value == "premium"
+)
+
+isCustomerService && hasDelegation && customerIsPremium
+```
+
+## API Request Mapping
+
+When you make an API request:
+
+```json
+{
+ "authenticated_user_id": "alice@example.com",
+ "on_behalf_of_user_id": "bob@example.com",
+ "user_id": "charlie@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "acc-123",
+ "transaction_id": "txn-456",
+ "transaction_request_id": "tr-123",
+ "customer_id": "cust-789"
+}
+```
+
+The engine automatically:
+1. Fetches `authenticatedUser` using `authenticated_user_id` (or from auth token if not provided)
+2. Fetches `authenticatedUserAttributes` and `authenticatedUserAuthContext` for authenticated user
+3. Fetches `onBehalfOfUserOpt`, `onBehalfOfUserAttributes`, `onBehalfOfUserAuthContext` if `on_behalf_of_user_id` provided
+4. Fetches `userOpt` and `userAttributes` if `user_id` provided
+5. Fetches `bankOpt` and `bankAttributes` if `bank_id` provided
+6. Fetches `accountOpt` and `accountAttributes` if `account_id` provided
+7. Fetches `transactionOpt` and `transactionAttributes` if `transaction_id` provided
+8. Fetches `transactionRequestOpt` and `transactionRequestAttributes` if `transaction_request_id` provided
+9. Fetches `customerOpt` and `customerAttributes` if `customer_id` provided
+
+## Working with Attributes
+
+All attribute lists follow the same pattern:
+
+```scala
+// Check if attribute exists with specific value
+attributeList.exists(attr => attr.name == "key" && attr.value == "value")
+
+// Check if list is empty
+attributeList.isEmpty
+
+// Check if list has any attributes
+attributeList.nonEmpty
+
+// Find specific attribute
+attributeList.find(_.name == "key").map(_.value)
+
+// Multiple attributes (AND)
+val hasAttr1 = attributeList.exists(_.name == "key1")
+val hasAttr2 = attributeList.exists(_.name == "key2")
+hasAttr1 && hasAttr2
+
+// Multiple attributes (OR)
+attributeList.exists(attr =>
+ List("key1", "key2", "key3").contains(attr.name)
+)
+```
+
+## Key Points
+
+✅ **18 parameters total** - comprehensive context for access decisions
+✅ **3 always available objects** - authenticatedUser, authenticatedUserAttributes, authenticatedUserAuthContext
+✅ **15 contextual parameters** - available based on what IDs are provided in the request
+✅ **All READ-ONLY** - cannot modify any parameter values
+✅ **Automatic fetching** - engine fetches all data based on provided IDs
+✅ **Type safety** - optional objects use `Option[T]`, lists are `List[T]`
+✅ **Empty lists not None** - attribute lists are always available, just empty when no data
+
+## Summary
+
+ABAC rules have access to:
+- **3 user contexts**: authenticated, onBehalfOf, and target user
+- **5 resource contexts**: bank, account, transaction, transaction request, customer
+- **Complete attribute data**: for all users and resources
+- **Auth context**: session, IP, device info, etc.
+- **Full type safety**: optional objects and guaranteed lists
+
+This provides everything needed to make sophisticated access control decisions!
+
+---
+
+**Related Documentation:**
+- `ABAC_OBJECT_PROPERTIES_REFERENCE.md` - Detailed property reference for each object
+- `ABAC_SIMPLE_GUIDE.md` - Getting started guide
+- `ABAC_REFACTORING.md` - Technical implementation details
+
+**Last Updated:** 2024
\ No newline at end of file
diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md b/obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md
new file mode 100644
index 000000000..e4815df12
--- /dev/null
+++ b/obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md
@@ -0,0 +1,354 @@
+# ABAC Rules Engine - Simple Guide
+
+## Overview
+
+The ABAC (Attribute-Based Access Control) Rules Engine allows you to create dynamic access control rules in Scala that evaluate whether a user should have access to a resource.
+
+## Core Concept
+
+**One Rule + One Execution Method = Simple Access Control**
+
+```scala
+def executeRule(
+ ruleId: String,
+ authenticatedUserId: String,
+ onBehalfOfUserId: Option[String] = None,
+ userId: Option[String] = None,
+ callContext: Option[CallContext] = None,
+ bankId: Option[String] = None,
+ accountId: Option[String] = None,
+ viewId: Option[String] = None,
+ transactionId: Option[String] = None,
+ customerId: Option[String] = None
+): Box[Boolean]
+```
+
+---
+
+## Understanding the Three User Parameters
+
+### 1. `authenticatedUserId` (Required)
+**The person actually logged in and making the API call**
+
+- This is ALWAYS the real user who authenticated
+- Retrieved from the authentication token
+- Cannot be faked or changed
+
+**Example:** Alice logs into the banking app
+- `authenticatedUserId = "alice@example.com"`
+
+---
+
+### 2. `onBehalfOfUserId` (Optional)
+**When someone acts on behalf of another user (delegation)**
+
+- Used for delegation scenarios
+- The authenticated user is acting for someone else
+- Common in customer service, admin tools, power of attorney
+
+**Example:** Customer service rep Bob helps Alice with her account
+- `authenticatedUserId = "bob@customerservice.com"` (the rep logged in)
+- `onBehalfOfUserId = "alice@example.com"` (helping Alice)
+- `userId = "alice@example.com"` (checking Alice's permissions)
+
+---
+
+### 3. `userId` (Optional)
+**The target user being evaluated by the rule**
+
+- Defaults to `authenticatedUserId` if not provided
+- The user whose permissions/attributes are being checked
+- Useful for testing rules for different users
+
+**Example:** Admin checking if Alice can access an account
+- `authenticatedUserId = "admin@example.com"` (admin is logged in)
+- `userId = "alice@example.com"` (checking Alice's access)
+
+---
+
+## Common Scenarios
+
+### Scenario 1: Normal User Access
+**Alice wants to view her own account**
+
+```json
+{
+ "bank_id": "gh.29.uk",
+ "account_id": "alice-account-123"
+}
+```
+
+Behind the scenes:
+- `authenticatedUserId = "alice@example.com"` (from auth token)
+- `onBehalfOfUserId = None`
+- `userId = None` → defaults to Alice
+
+**Rule example:**
+```scala
+// Check if user owns the account
+accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+```
+
+---
+
+### Scenario 2: Customer Service Delegation
+**Bob (customer service) helps Alice view her account**
+
+```json
+{
+ "on_behalf_of_user_id": "alice@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "alice-account-123"
+}
+```
+
+Behind the scenes:
+- `authenticatedUserId = "bob@customerservice.com"` (from auth token)
+- `onBehalfOfUserId = "alice@example.com"`
+- `userId = None` → defaults to Bob, but rule can check both
+
+**Rule example:**
+```scala
+// Allow if authenticated user is customer service AND acting on behalf of an account owner
+val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com")
+val hasValidDelegation = onBehalfOfUserOpt.isDefined
+val targetOwnsAccount = accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+
+isCustomerService && hasValidDelegation && targetOwnsAccount
+```
+
+---
+
+### Scenario 3: Admin Testing
+**Admin wants to test if Alice can access an account (without logging in as Alice)**
+
+```json
+{
+ "user_id": "alice@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "alice-account-123"
+}
+```
+
+Behind the scenes:
+- `authenticatedUserId = "admin@example.com"` (from auth token)
+- `onBehalfOfUserId = None`
+- `userId = "alice@example.com"` (evaluating for Alice)
+
+**Rule example:**
+```scala
+// Allow admins to test access, or allow if user owns account
+val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com")
+val userOwnsAccount = accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+
+isAdmin || userOwnsAccount
+```
+
+---
+
+## API Usage
+
+### Endpoint
+```
+POST /obp/v6.0.0/management/abac-rules/{RULE_ID}/execute
+```
+
+### Request Examples
+
+#### Example 1: Basic Access Check
+```json
+{
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+}
+```
+- Checks if authenticated user can access the account
+
+#### Example 2: Delegation
+```json
+{
+ "on_behalf_of_user_id": "alice@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+}
+```
+- Authenticated user acting on behalf of Alice
+
+#### Example 3: Testing for Different User
+```json
+{
+ "user_id": "bob@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+}
+```
+- Check if Bob can access the account (useful for admins testing)
+
+#### Example 4: Complex Scenario
+```json
+{
+ "on_behalf_of_user_id": "alice@example.com",
+ "user_id": "charlie@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0",
+ "transaction_id": "trans-123"
+}
+```
+- Authenticated user acting on behalf of Alice
+- Checking if Charlie can access account and transaction
+
+---
+
+## Writing ABAC Rules
+
+### Available Objects in Rules
+
+```scala
+// These are available in your rule code:
+authenticatedUser: User // Always present - the logged in user
+onBehalfOfUserOpt: Option[User] // Present if delegation
+user: User // Always present - the target user being evaluated
+bankOpt: Option[Bank] // Present if bank_id provided
+accountOpt: Option[BankAccount] // Present if account_id provided
+transactionOpt: Option[Transaction] // Present if transaction_id provided
+customerOpt: Option[Customer] // Present if customer_id provided
+```
+
+### Simple Rule Examples
+
+#### Rule 1: User Must Own Account
+```scala
+accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+```
+
+#### Rule 2: Admin or Owner
+```scala
+val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com")
+val isOwner = accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+
+isAdmin || isOwner
+```
+
+#### Rule 3: Customer Service Delegation
+```scala
+val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com")
+val actingOnBehalf = onBehalfOfUserOpt.isDefined
+val userIsOwner = accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+
+// Allow if customer service is helping an account owner
+isCustomerService && actingOnBehalf && userIsOwner
+```
+
+#### Rule 4: Self-Service Only (No Delegation)
+```scala
+// User must be checking their own access (no delegation allowed)
+val isSelfService = authenticatedUser.userId == user.userId
+val noDelegation = onBehalfOfUserOpt.isEmpty
+
+isSelfService && noDelegation
+```
+
+#### Rule 5: Account Balance Check
+```scala
+accountOpt.exists(account => account.balance.toDouble >= 1000.0)
+```
+
+---
+
+## Quick Reference Table
+
+| Parameter | Required? | Purpose | Example Value |
+|-----------|-----------|---------|---------------|
+| `authenticatedUserId` | ✅ Yes | Who is logged in | `"alice@example.com"` |
+| `onBehalfOfUserId` | ❌ Optional | Delegation | `"bob@example.com"` |
+| `userId` | ❌ Optional | Target user to evaluate | `"charlie@example.com"` |
+| `bankId` | ❌ Optional | Bank context | `"gh.29.uk"` |
+| `accountId` | ❌ Optional | Account context | `"acc-123"` |
+| `viewId` | ❌ Optional | View context | `"owner"` |
+| `transactionId` | ❌ Optional | Transaction context | `"trans-456"` |
+| `customerId` | ❌ Optional | Customer context | `"cust-789"` |
+
+---
+
+## Real-World Use Cases
+
+### Use Case 1: Personal Banking
+- User logs in → `authenticatedUserId`
+- Views their own account → `userId` defaults to authenticated user
+- Rule checks ownership
+
+### Use Case 2: Business Banking with Delegates
+- CFO logs in → `authenticatedUserId = "cfo@company.com"`
+- Checks on behalf of CEO → `onBehalfOfUserId = "ceo@company.com"`
+- System evaluates if CEO has access → `userId = "ceo@company.com"`
+
+### Use Case 3: Customer Support
+- Support agent logs in → `authenticatedUserId = "agent@bank.com"`
+- Helps customer → `onBehalfOfUserId = "customer@example.com"`
+- Rule verifies: agent has support role AND customer owns account
+
+### Use Case 4: Admin Panel
+- Admin logs in → `authenticatedUserId = "admin@bank.com"`
+- Tests rule for any user → `userId = "testuser@example.com"`
+- Rule evaluates for test user, but admin must be authenticated
+
+---
+
+## Testing Tips
+
+### Test Different Users
+```bash
+# Test as yourself
+curl -X POST .../execute -d '{"bank_id": "gh.29.uk"}'
+
+# Test for another user (if you have permission)
+curl -X POST .../execute -d '{"user_id": "other@example.com", "bank_id": "gh.29.uk"}'
+```
+
+### Test Delegation
+```bash
+# Act on behalf of someone
+curl -X POST .../execute -d '{
+ "on_behalf_of_user_id": "alice@example.com",
+ "bank_id": "gh.29.uk"
+}'
+```
+
+### Debug Your Rules
+```scala
+// Add simple checks to understand what's happening
+val result = (authenticatedUser.userId == user.userId)
+println(s"Auth user: ${authenticatedUser.userId}, Target user: ${user.userId}, Match: $result")
+result
+```
+
+---
+
+## Summary
+
+✅ **Keep it simple**: One execution method, clear parameters
+✅ **Three user IDs**: authenticated (who), on-behalf-of (delegation), user (target)
+✅ **Write rules in Scala**: Full power of the language
+✅ **Test via API**: Just pass IDs, objects fetched automatically
+✅ **Flexible**: Supports normal access, delegation, and admin testing
+
+---
+
+**Related Documentation:**
+- `ABAC_OBJECT_PROPERTIES_REFERENCE.md` - Full list of available properties
+- `ABAC_TESTING_EXAMPLES.md` - More testing examples
+- `ABAC_REFACTORING.md` - Technical implementation details
+
+**Last Updated:** 2024
\ No newline at end of file
diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md b/obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md
new file mode 100644
index 000000000..b1a64564f
--- /dev/null
+++ b/obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md
@@ -0,0 +1,622 @@
+# ABAC Rule Testing Examples
+
+This document provides practical examples for testing ABAC (Attribute-Based Access Control) rules using the refactored ID-based API.
+
+## Prerequisites
+
+1. You need a valid DirectLogin token or other authentication method
+2. You must have the `canExecuteAbacRule` entitlement
+3. You need to know the IDs of:
+ - ABAC rules you want to test
+ - Users, banks, accounts, transactions, customers (as needed by your rules)
+
+## API Endpoint
+
+```
+POST /obp/v6.0.0/management/abac-rules/{RULE_ID}/execute
+```
+
+## Basic Examples
+
+### Example 1: Simple User-Only Rule
+
+Test a rule that only checks user attributes (no bank/account context needed).
+
+**Rule Code:**
+```scala
+// Rule: Only allow admin users
+user.userId.endsWith("@admin.com")
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/admin-only-rule/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{}'
+```
+
+**Response:**
+```json
+{
+ "rule_id": "admin-only-rule",
+ "rule_name": "Admin Only Access",
+ "result": true,
+ "message": "Access granted"
+}
+```
+
+### Example 2: Test Rule for Different User
+
+Test how the rule behaves for a different user (without re-authenticating).
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/admin-only-rule/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "user_id": "alice@example.com"
+ }'
+```
+
+**Response:**
+```json
+{
+ "rule_id": "admin-only-rule",
+ "rule_name": "Admin Only Access",
+ "result": false,
+ "message": "Access denied"
+}
+```
+
+### Example 3: Bank-Specific Rule
+
+Test a rule that checks bank context.
+
+**Rule Code:**
+```scala
+// Rule: Only allow access to UK banks
+bankOpt.exists(bank =>
+ bank.bankId.value.startsWith("gh.") ||
+ bank.bankId.value.startsWith("uk.")
+)
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/uk-banks-only/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "bank_id": "gh.29.uk"
+ }'
+```
+
+**Response:**
+```json
+{
+ "rule_id": "uk-banks-only",
+ "rule_name": "UK Banks Only",
+ "result": true,
+ "message": "Access granted"
+}
+```
+
+### Example 4: Account Balance Rule
+
+Test a rule that checks account balance.
+
+**Rule Code:**
+```scala
+// Rule: Only allow if account balance > 1000
+accountOpt.exists(account =>
+ account.balance.toDouble > 1000.0
+)
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/high-balance-only/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+ }'
+```
+
+**Response:**
+```json
+{
+ "rule_id": "high-balance-only",
+ "rule_name": "High Balance Only",
+ "result": true,
+ "message": "Access granted"
+}
+```
+
+### Example 5: Account Ownership Rule
+
+Test a rule that checks if user owns the account.
+
+**Rule Code:**
+```scala
+// Rule: User must own the account
+accountOpt.exists(account =>
+ account.owners.exists(owner => owner.userId == user.userId)
+)
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/account-owner-only/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "user_id": "alice@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+ }'
+```
+
+### Example 6: Transaction Amount Rule
+
+Test a rule that checks transaction amount.
+
+**Rule Code:**
+```scala
+// Rule: Only allow transactions under 10000
+transactionOpt.exists(txn =>
+ txn.amount.toDouble < 10000.0
+)
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/small-transactions/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0",
+ "transaction_id": "trans-123"
+ }'
+```
+
+### Example 7: Customer Credit Rating Rule
+
+Test a rule that checks customer credit rating.
+
+**Rule Code:**
+```scala
+// Rule: Only allow customers with excellent credit
+customerOpt.exists(customer =>
+ customer.creditRating.getOrElse("") == "EXCELLENT"
+)
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/excellent-credit-only/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "bank_id": "gh.29.uk",
+ "customer_id": "cust-456"
+ }'
+```
+
+## Complex Examples
+
+### Example 8: Multi-Condition Rule
+
+Test a complex rule with multiple conditions.
+
+**Rule Code:**
+```scala
+// Rule: Allow if:
+// - User is admin, OR
+// - User owns account AND balance > 100 AND account is at UK bank
+val isAdmin = user.userId.endsWith("@admin.com")
+val ownsAccount = accountOpt.exists(_.owners.exists(_.userId == user.userId))
+val hasBalance = accountOpt.exists(_.balance.toDouble > 100.0)
+val isUKBank = bankOpt.exists(b =>
+ b.bankId.value.startsWith("gh.") || b.bankId.value.startsWith("uk.")
+)
+
+isAdmin || (ownsAccount && hasBalance && isUKBank)
+```
+
+**Test Request (Admin User):**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/complex-access/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "user_id": "admin@admin.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+ }'
+```
+
+**Test Request (Regular User):**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/complex-access/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "user_id": "alice@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"
+ }'
+```
+
+### Example 9: Time-Based Rule
+
+Test a rule that includes time-based logic.
+
+**Rule Code:**
+```scala
+// Rule: Only allow during business hours (9 AM - 5 PM) unless user is admin
+import java.time.LocalTime
+import java.time.ZoneId
+
+val now = LocalTime.now(ZoneId.of("Europe/London"))
+val isBusinessHours = now.isAfter(LocalTime.of(9, 0)) && now.isBefore(LocalTime.of(17, 0))
+val isAdmin = user.userId.endsWith("@admin.com")
+
+isAdmin || isBusinessHours
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/business-hours-only/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "user_id": "alice@example.com"
+ }'
+```
+
+### Example 10: Cross-Entity Validation
+
+Test a rule that validates relationships between entities.
+
+**Rule Code:**
+```scala
+// Rule: Customer must be associated with the same bank as the account
+(customerOpt, accountOpt, bankOpt) match {
+ case (Some(customer), Some(account), Some(bank)) =>
+ customer.bankId == bank.bankId &&
+ account.bankId == bank.bankId
+ case _ => false
+}
+```
+
+**Test Request:**
+```bash
+curl -X POST \
+ 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/cross-entity-validation/execute' \
+ -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0",
+ "customer_id": "cust-456"
+ }'
+```
+
+## Testing Patterns
+
+### Pattern 1: Test Multiple Users
+
+Test the same rule for different users to verify behavior:
+
+```bash
+# Test for admin
+curl -X POST 'https://.../execute' -d '{"user_id": "admin@admin.com", "bank_id": "gh.29.uk"}'
+
+# Test for regular user
+curl -X POST 'https://.../execute' -d '{"user_id": "alice@example.com", "bank_id": "gh.29.uk"}'
+
+# Test for another user
+curl -X POST 'https://.../execute' -d '{"user_id": "bob@example.com", "bank_id": "gh.29.uk"}'
+```
+
+### Pattern 2: Test Different Banks
+
+Test how the rule behaves across different banks:
+
+```bash
+# UK Bank
+curl -X POST 'https://.../execute' -d '{"bank_id": "gh.29.uk", "account_id": "acc1"}'
+
+# US Bank
+curl -X POST 'https://.../execute' -d '{"bank_id": "us.bank.01", "account_id": "acc2"}'
+
+# German Bank
+curl -X POST 'https://.../execute' -d '{"bank_id": "de.bank.01", "account_id": "acc3"}'
+```
+
+### Pattern 3: Test Edge Cases
+
+Test boundary conditions:
+
+```bash
+# No context (minimal)
+curl -X POST 'https://.../execute' -d '{}'
+
+# Partial context
+curl -X POST 'https://.../execute' -d '{"bank_id": "gh.29.uk"}'
+
+# Full context
+curl -X POST 'https://.../execute' -d '{
+ "user_id": "alice@example.com",
+ "bank_id": "gh.29.uk",
+ "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0",
+ "transaction_id": "trans-123",
+ "customer_id": "cust-456"
+}'
+
+# Invalid IDs (should handle gracefully)
+curl -X POST 'https://.../execute' -d '{"bank_id": "invalid-bank-id"}'
+```
+
+### Pattern 4: Automated Testing Script
+
+Create a bash script to test multiple scenarios:
+
+```bash
+#!/bin/bash
+
+API_BASE="https://api.openbankproject.com/obp/v6.0.0"
+TOKEN="eyJhbGciOiJIUzI1..."
+RULE_ID="my-test-rule"
+
+test_rule() {
+ local description=$1
+ local payload=$2
+
+ echo "Testing: $description"
+ curl -s -X POST \
+ "$API_BASE/management/abac-rules/$RULE_ID/execute" \
+ -H "Authorization: DirectLogin token=$TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "$payload" | jq '.result, .message'
+ echo "---"
+}
+
+# Run tests
+test_rule "Admin user" '{"user_id": "admin@admin.com"}'
+test_rule "Regular user" '{"user_id": "alice@example.com"}'
+test_rule "With bank context" '{"user_id": "alice@example.com", "bank_id": "gh.29.uk"}'
+test_rule "With account context" '{"user_id": "alice@example.com", "bank_id": "gh.29.uk", "account_id": "acc1"}'
+```
+
+## Error Scenarios
+
+### Error 1: Rule Not Found
+
+```bash
+curl -X POST 'https://.../management/abac-rules/nonexistent-rule/execute' \
+ -H 'Authorization: DirectLogin token=...' \
+ -d '{}'
+```
+
+**Response:**
+```json
+{
+ "code": 404,
+ "message": "ABAC Rule not found with ID: nonexistent-rule"
+}
+```
+
+### Error 2: Inactive Rule
+
+If the rule exists but is not active:
+
+**Response:**
+```json
+{
+ "rule_id": "inactive-rule",
+ "rule_name": "Inactive Rule",
+ "result": false,
+ "message": "Execution error: ABAC Rule Inactive Rule is not active"
+}
+```
+
+### Error 3: Invalid User ID
+
+```bash
+curl -X POST 'https://.../execute' \
+ -H 'Authorization: DirectLogin token=...' \
+ -d '{"user_id": "nonexistent-user"}'
+```
+
+**Response:**
+```json
+{
+ "rule_id": "test-rule",
+ "rule_name": "Test Rule",
+ "result": false,
+ "message": "Execution error: User not found"
+}
+```
+
+### Error 4: Compilation Error
+
+If the rule has invalid Scala code:
+
+**Response:**
+```json
+{
+ "rule_id": "broken-rule",
+ "rule_name": "Broken Rule",
+ "result": false,
+ "message": "Execution error: Failed to compile ABAC rule: ..."
+}
+```
+
+## Python Testing Example
+
+```python
+import requests
+import json
+
+class AbacRuleTester:
+ def __init__(self, base_url, token):
+ self.base_url = base_url
+ self.headers = {
+ 'Authorization': f'DirectLogin token={token}',
+ 'Content-Type': 'application/json'
+ }
+
+ def test_rule(self, rule_id, **context):
+ """Test an ABAC rule with given context"""
+ url = f"{self.base_url}/management/abac-rules/{rule_id}/execute"
+
+ # Filter out None values
+ payload = {k: v for k, v in context.items() if v is not None}
+
+ response = requests.post(url, headers=self.headers, json=payload)
+ return response.json()
+
+ def test_users(self, rule_id, user_ids, **context):
+ """Test rule for multiple users"""
+ results = {}
+ for user_id in user_ids:
+ result = self.test_rule(rule_id, user_id=user_id, **context)
+ results[user_id] = result['result']
+ return results
+
+# Usage
+tester = AbacRuleTester(
+ base_url='https://api.openbankproject.com/obp/v6.0.0',
+ token='your-token-here'
+)
+
+# Test single rule
+result = tester.test_rule(
+ 'admin-only-rule',
+ user_id='alice@example.com',
+ bank_id='gh.29.uk'
+)
+print(f"Result: {result['result']}, Message: {result['message']}")
+
+# Test multiple users
+users = ['admin@admin.com', 'alice@example.com', 'bob@example.com']
+results = tester.test_users('account-owner-rule', users,
+ bank_id='gh.29.uk',
+ account_id='acc123')
+print(results)
+# Output: {'admin@admin.com': True, 'alice@example.com': False, ...}
+```
+
+## JavaScript Testing Example
+
+```javascript
+class AbacRuleTester {
+ constructor(baseUrl, token) {
+ this.baseUrl = baseUrl;
+ this.headers = {
+ 'Authorization': `DirectLogin token=${token}`,
+ 'Content-Type': 'application/json'
+ };
+ }
+
+ async testRule(ruleId, context = {}) {
+ const url = `${this.baseUrl}/management/abac-rules/${ruleId}/execute`;
+
+ // Remove undefined values
+ const payload = Object.fromEntries(
+ Object.entries(context).filter(([_, v]) => v !== undefined)
+ );
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: this.headers,
+ body: JSON.stringify(payload)
+ });
+
+ return await response.json();
+ }
+
+ async testUsers(ruleId, userIds, context = {}) {
+ const results = {};
+ for (const userId of userIds) {
+ const result = await this.testRule(ruleId, { ...context, user_id: userId });
+ results[userId] = result.result;
+ }
+ return results;
+ }
+}
+
+// Usage
+const tester = new AbacRuleTester(
+ 'https://api.openbankproject.com/obp/v6.0.0',
+ 'your-token-here'
+);
+
+// Test single rule
+const result = await tester.testRule('admin-only-rule', {
+ user_id: 'alice@example.com',
+ bank_id: 'gh.29.uk'
+});
+console.log(`Result: ${result.result}, Message: ${result.message}`);
+
+// Test multiple users
+const users = ['admin@admin.com', 'alice@example.com', 'bob@example.com'];
+const results = await tester.testUsers('account-owner-rule', users, {
+ bank_id: 'gh.29.uk',
+ account_id: 'acc123'
+});
+console.log(results);
+```
+
+## Best Practices
+
+1. **Start Simple**: Begin with rules that only check user attributes, then add complexity
+2. **Test Edge Cases**: Always test with missing IDs, invalid IDs, and partial context
+3. **Test Multiple Users**: Verify rule behavior for different user types (admin, owner, guest)
+4. **Use Automation**: Create scripts to test multiple scenarios quickly
+5. **Document Expected Behavior**: Keep track of what each test should return
+6. **Test Both Paths**: Test cases that should allow access AND cases that should deny
+7. **Performance Testing**: Test with realistic data volumes to ensure rules perform well
+
+## Troubleshooting
+
+### Rule Always Returns False
+
+- Check if the rule is active (`is_active: true`)
+- Verify the rule code compiles successfully
+- Ensure all required context IDs are provided
+- Check if objects are being fetched successfully
+
+### Rule Times Out
+
+- Rule execution has a 5-second timeout for object fetching
+- Simplify rule logic or optimize database queries
+- Consider caching frequently accessed objects
+
+### Unexpected Results
+
+- Test with `executeRuleWithObjects` to verify rule logic
+- Check object availability (might be `None` if fetch fails)
+- Add logging to rule code to debug decision logic
+- Verify IDs are correct and objects exist in database
+
+---
+
+**Last Updated:** 2024
+**Related Documentation:** ABAC_REFACTORING.md
\ No newline at end of file
diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template
index 087163b68..f9416680e 100644
--- a/obp-api/src/main/resources/props/sample.props.template
+++ b/obp-api/src/main/resources/props/sample.props.template
@@ -1683,3 +1683,13 @@ securelogging_mask_credit_card=true
# Email addresses
securelogging_mask_email=true
+
+
+############################################
+# http4s server configuration
+############################################
+
+# Host and port for http4s server (used by bootstrap.http4s.Http4sServer)
+# Defaults (if not set) are 127.0.0.1 and 8181
+http4s.host=127.0.0.1
+http4s.port=8086
\ No newline at end of file
diff --git a/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala b/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala
index c11df9f11..c3865de35 100644
--- a/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala
+++ b/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala
@@ -1,14 +1,19 @@
package code.abacrule
-import code.api.util.{APIUtil, DynamicUtil}
+import code.api.util.{APIUtil, CallContext, DynamicUtil}
+import code.bankconnectors.Connector
import code.model.dataAccess.ResourceUser
+import code.users.Users
import com.openbankproject.commons.model._
+import com.openbankproject.commons.ExecutionContext.Implicits.global
import net.liftweb.common.{Box, Empty, Failure, Full}
import net.liftweb.util.Helpers.tryo
import java.util.concurrent.ConcurrentHashMap
import scala.collection.JavaConverters._
import scala.collection.concurrent
+import scala.concurrent.Await
+import scala.concurrent.duration._
/**
* ABAC Rule Engine for compiling and executing Attribute-Based Access Control rules
@@ -21,10 +26,12 @@ object AbacRuleEngine {
/**
* Type alias for compiled ABAC rule function
- * Parameters: User, Option[Bank], Option[Account], Option[Transaction], Option[Customer]
+ * Parameters: authenticatedUser (logged in), authenticatedUserAttributes (non-personal), authenticatedUserAuthContext (auth context),
+ * onBehalfOfUser (delegation), onBehalfOfUserAttributes, onBehalfOfUserAuthContext,
+ * user, userAttributes, bankOpt, bankAttributes, accountOpt, accountAttributes, transactionOpt, transactionAttributes, customerOpt, customerAttributes
* Returns: Boolean (true = allow access, false = deny access)
*/
- type AbacRuleFunction = (User, Option[Bank], Option[BankAccount], Option[Transaction], Option[Customer]) => Boolean
+ type AbacRuleFunction = (User, List[UserAttributeTrait], List[UserAuthContext], Option[User], List[UserAttributeTrait], List[UserAuthContext], Option[User], List[UserAttributeTrait], Option[Bank], List[BankAttributeTrait], Option[BankAccount], List[AccountAttribute], Option[Transaction], List[TransactionAttribute], Option[TransactionRequest], List[TransactionRequestAttributeTrait], Option[Customer], List[CustomerAttribute]) => Boolean
/**
* Compile an ABAC rule from Scala code
@@ -68,14 +75,212 @@ object AbacRuleEngine {
|import net.liftweb.common._
|
|// ABAC Rule Function
- |(user: User, bankOpt: Option[Bank], accountOpt: Option[BankAccount], transactionOpt: Option[Transaction], customerOpt: Option[Customer]) => {
+ |(authenticatedUser: User, authenticatedUserAttributes: List[UserAttributeTrait], authenticatedUserAuthContext: List[UserAuthContext], onBehalfOfUserOpt: Option[User], onBehalfOfUserAttributes: List[UserAttributeTrait], onBehalfOfUserAuthContext: List[UserAuthContext], userOpt: Option[User], userAttributes: List[UserAttributeTrait], bankOpt: Option[Bank], bankAttributes: List[BankAttributeTrait], accountOpt: Option[BankAccount], accountAttributes: List[AccountAttribute], transactionOpt: Option[Transaction], transactionAttributes: List[TransactionAttribute], transactionRequestOpt: Option[TransactionRequest], transactionRequestAttributes: List[TransactionRequestAttributeTrait], customerOpt: Option[Customer], customerAttributes: List[CustomerAttribute]) => {
| $ruleCode
|}
|""".stripMargin
}
/**
- * Execute an ABAC rule
+ * Execute an ABAC rule by IDs (objects are fetched internally)
+ *
+ * @param ruleId The ID of the rule to execute
+ * @param authenticatedUserId The ID of the authenticated user (the person logged in)
+ * @param onBehalfOfUserId Optional ID of user being acted on behalf of (delegation scenario)
+ * @param userId The ID of the target user to evaluate (defaults to authenticated user if not provided)
+ * @param callContext Call context for fetching objects
+ * @param bankId Optional bank ID
+ * @param accountId Optional account ID
+ * @param viewId Optional view ID (for future use)
+ * @param transactionId Optional transaction ID
+ * @param transactionRequestId Optional transaction request ID
+ * @param customerId Optional customer ID
+ * @return Box[Boolean] - Full(true) if allowed, Full(false) if denied, Failure on error
+ */
+ def executeRule(
+ ruleId: String,
+ authenticatedUserId: String,
+ onBehalfOfUserId: Option[String] = None,
+ userId: Option[String] = None,
+ callContext: Option[CallContext] = None,
+ bankId: Option[String] = None,
+ accountId: Option[String] = None,
+ viewId: Option[String] = None,
+ transactionId: Option[String] = None,
+ transactionRequestId: Option[String] = None,
+ customerId: Option[String] = None
+ ): Box[Boolean] = {
+ for {
+ rule <- MappedAbacRuleProvider.getAbacRuleById(ruleId)
+ _ <- if (rule.isActive) Full(true) else Failure(s"ABAC Rule ${rule.ruleName} is not active")
+
+ // Fetch authenticated user (the actual person logged in)
+ authenticatedUser <- Users.users.vend.getUserByUserId(authenticatedUserId)
+
+ // Fetch non-personal attributes for authenticated user
+ authenticatedUserAttributes = Await.result(
+ code.api.util.NewStyle.function.getNonPersonalUserAttributes(authenticatedUserId, callContext).map(_._1),
+ 5.seconds
+ )
+
+ // Fetch auth context for authenticated user
+ authenticatedUserAuthContext = Await.result(
+ code.api.util.NewStyle.function.getUserAuthContexts(authenticatedUserId, callContext).map(_._1),
+ 5.seconds
+ )
+
+ // Fetch onBehalfOf user if provided (delegation scenario)
+ onBehalfOfUserOpt <- onBehalfOfUserId match {
+ case Some(obUserId) => Users.users.vend.getUserByUserId(obUserId).map(Some(_))
+ case None => Full(None)
+ }
+
+ // Fetch attributes for onBehalfOf user if provided
+ onBehalfOfUserAttributes = onBehalfOfUserId match {
+ case Some(obUserId) =>
+ Await.result(
+ code.api.util.NewStyle.function.getNonPersonalUserAttributes(obUserId, callContext).map(_._1),
+ 5.seconds
+ )
+ case None => List.empty[UserAttributeTrait]
+ }
+
+ // Fetch auth context for onBehalfOf user if provided
+ onBehalfOfUserAuthContext = onBehalfOfUserId match {
+ case Some(obUserId) =>
+ Await.result(
+ code.api.util.NewStyle.function.getUserAuthContexts(obUserId, callContext).map(_._1),
+ 5.seconds
+ )
+ case None => List.empty[UserAuthContext]
+ }
+
+ // Fetch target user if userId is provided
+ userOpt <- userId match {
+ case Some(uId) => Users.users.vend.getUserByUserId(uId).map(Some(_))
+ case None => Full(None)
+ }
+
+ // Fetch attributes for target user if provided
+ userAttributes = userId match {
+ case Some(uId) =>
+ Await.result(
+ code.api.util.NewStyle.function.getNonPersonalUserAttributes(uId, callContext).map(_._1),
+ 5.seconds
+ )
+ case None => List.empty[UserAttributeTrait]
+ }
+
+ // Fetch bank if bankId is provided
+ bankOpt <- bankId match {
+ case Some(bId) =>
+ tryo(Await.result(
+ code.api.util.NewStyle.function.getBank(BankId(bId), callContext).map(_._1),
+ 5.seconds
+ )).map(Some(_))
+ case None => Full(None)
+ }
+
+ // Fetch bank attributes if bank is provided
+ bankAttributes = bankId match {
+ case Some(bId) =>
+ Await.result(
+ code.api.util.NewStyle.function.getBankAttributesByBank(BankId(bId), callContext).map(_._1),
+ 5.seconds
+ )
+ case None => List.empty[BankAttributeTrait]
+ }
+
+ // Fetch account if accountId and bankId are provided
+ accountOpt <- (bankId, accountId) match {
+ case (Some(bId), Some(aId)) =>
+ tryo(Await.result(
+ code.api.util.NewStyle.function.getBankAccount(BankId(bId), AccountId(aId), callContext).map(_._1),
+ 5.seconds
+ )).map(Some(_))
+ case _ => Full(None)
+ }
+
+ // Fetch account attributes if account is provided
+ accountAttributes = (bankId, accountId) match {
+ case (Some(bId), Some(aId)) =>
+ Await.result(
+ code.api.util.NewStyle.function.getAccountAttributesByAccount(BankId(bId), AccountId(aId), callContext).map(_._1),
+ 5.seconds
+ )
+ case _ => List.empty[AccountAttribute]
+ }
+
+ // Fetch transaction if transactionId, accountId, and bankId are provided
+ transactionOpt <- (bankId, accountId, transactionId) match {
+ case (Some(bId), Some(aId), Some(tId)) =>
+ tryo(Await.result(
+ code.api.util.NewStyle.function.getTransaction(BankId(bId), AccountId(aId), TransactionId(tId), callContext).map(_._1),
+ 5.seconds
+ )).map(trans => Some(trans))
+ case _ => Full(None)
+ }
+
+ // Fetch transaction attributes if transaction is provided
+ transactionAttributes = (bankId, transactionId) match {
+ case (Some(bId), Some(tId)) =>
+ Await.result(
+ code.api.util.NewStyle.function.getTransactionAttributes(BankId(bId), TransactionId(tId), callContext).map(_._1),
+ 5.seconds
+ )
+ case _ => List.empty[TransactionAttribute]
+ }
+
+ // Fetch transaction request if transactionRequestId is provided
+ transactionRequestOpt <- transactionRequestId match {
+ case Some(trId) =>
+ tryo(Await.result(
+ code.api.util.NewStyle.function.getTransactionRequestImpl(TransactionRequestId(trId), callContext).map(_._1),
+ 5.seconds
+ )).map(tr => Some(tr))
+ case _ => Full(None)
+ }
+
+ // Fetch transaction request attributes if transaction request is provided
+ transactionRequestAttributes = (bankId, transactionRequestId) match {
+ case (Some(bId), Some(trId)) =>
+ Await.result(
+ code.api.util.NewStyle.function.getTransactionRequestAttributes(BankId(bId), TransactionRequestId(trId), callContext).map(_._1),
+ 5.seconds
+ )
+ case _ => List.empty[TransactionRequestAttributeTrait]
+ }
+
+ // Fetch customer if customerId and bankId are provided
+ customerOpt <- (bankId, customerId) match {
+ case (Some(bId), Some(cId)) =>
+ tryo(Await.result(
+ code.api.util.NewStyle.function.getCustomerByCustomerId(cId, callContext).map(_._1),
+ 5.seconds
+ )).map(cust => Some(cust))
+ case _ => Full(None)
+ }
+
+ // Fetch customer attributes if customer is provided
+ customerAttributes = (bankId, customerId) match {
+ case (Some(bId), Some(cId)) =>
+ Await.result(
+ code.api.util.NewStyle.function.getCustomerAttributes(BankId(bId), CustomerId(cId), callContext).map(_._1),
+ 5.seconds
+ )
+ case _ => List.empty[CustomerAttribute]
+ }
+
+ // Compile and execute the rule
+ compiledFunc <- compileRule(ruleId, rule.ruleCode)
+ result <- tryo {
+ compiledFunc(authenticatedUser, authenticatedUserAttributes, authenticatedUserAuthContext, onBehalfOfUserOpt, onBehalfOfUserAttributes, onBehalfOfUserAuthContext, userOpt, userAttributes, bankOpt, bankAttributes, accountOpt, accountAttributes, transactionOpt, transactionAttributes, transactionRequestOpt, transactionRequestAttributes, customerOpt, customerAttributes)
+ }
+ } yield result
+ }
+
+ /**
+ * Execute an ABAC rule with pre-fetched objects (for backward compatibility and testing)
*
* @param ruleId The ID of the rule to execute
* @param user The user requesting access
@@ -85,7 +290,7 @@ object AbacRuleEngine {
* @param customerOpt Optional customer context
* @return Box[Boolean] - Full(true) if allowed, Full(false) if denied, Failure on error
*/
- def executeRule(
+ def executeRuleWithObjects(
ruleId: String,
user: User,
bankOpt: Option[Bank] = None,
@@ -98,96 +303,12 @@ object AbacRuleEngine {
_ <- if (rule.isActive) Full(true) else Failure(s"ABAC Rule ${rule.ruleName} is not active")
compiledFunc <- compileRule(ruleId, rule.ruleCode)
result <- tryo {
- // Execute rule function directly
- // Note: Sandbox execution can be added later if needed
- compiledFunc(user, bankOpt, accountOpt, transactionOpt, customerOpt)
+ compiledFunc(user, List.empty, List.empty, None, List.empty, List.empty, Some(user), List.empty, bankOpt, List.empty, accountOpt, List.empty, transactionOpt, List.empty, None, List.empty, customerOpt, List.empty)
}
} yield result
}
- /**
- * Execute multiple ABAC rules (AND logic - all must pass)
- *
- * @param ruleIds List of rule IDs to execute
- * @param user The user requesting access
- * @param bankOpt Optional bank context
- * @param accountOpt Optional account context
- * @param transactionOpt Optional transaction context
- * @param customerOpt Optional customer context
- * @return Box[Boolean] - Full(true) if all rules pass, Full(false) if any rule fails
- */
- def executeRulesAnd(
- ruleIds: List[String],
- user: User,
- bankOpt: Option[Bank] = None,
- accountOpt: Option[BankAccount] = None,
- transactionOpt: Option[Transaction] = None,
- customerOpt: Option[Customer] = None
- ): Box[Boolean] = {
- if (ruleIds.isEmpty) {
- Full(true) // No rules means allow by default
- } else {
- val results = ruleIds.map { ruleId =>
- executeRule(ruleId, user, bankOpt, accountOpt, transactionOpt, customerOpt)
- }
-
- // Check if any rule failed
- results.find(_.exists(_ == false)) match {
- case Some(_) => Full(false) // At least one rule denied access
- case None =>
- // Check if all succeeded
- if (results.forall(_.isDefined)) {
- Full(true) // All rules passed
- } else {
- // At least one rule had an error
- val errors = results.collect { case Failure(msg, _, _) => msg }
- Failure(s"ABAC rule execution errors: ${errors.mkString("; ")}")
- }
- }
- }
- }
- /**
- * Execute multiple ABAC rules (OR logic - at least one must pass)
- *
- * @param ruleIds List of rule IDs to execute
- * @param user The user requesting access
- * @param bankOpt Optional bank context
- * @param accountOpt Optional account context
- * @param transactionOpt Optional transaction context
- * @param customerOpt Optional customer context
- * @return Box[Boolean] - Full(true) if any rule passes, Full(false) if all rules fail
- */
- def executeRulesOr(
- ruleIds: List[String],
- user: User,
- bankOpt: Option[Bank] = None,
- accountOpt: Option[BankAccount] = None,
- transactionOpt: Option[Transaction] = None,
- customerOpt: Option[Customer] = None
- ): Box[Boolean] = {
- if (ruleIds.isEmpty) {
- Full(false) // No rules means deny by default for OR
- } else {
- val results = ruleIds.map { ruleId =>
- executeRule(ruleId, user, bankOpt, accountOpt, transactionOpt, customerOpt)
- }
-
- // Check if any rule passed
- results.find(_.exists(_ == true)) match {
- case Some(_) => Full(true) // At least one rule allowed access
- case None =>
- // All rules either failed or had errors
- if (results.exists(_.isDefined)) {
- Full(false) // All rules that executed denied access
- } else {
- // All rules had errors
- val errors = results.collect { case Failure(msg, _, _) => msg }
- Failure(s"All ABAC rules failed: ${errors.mkString("; ")}")
- }
- }
- }
- }
/**
* Validate ABAC rule code by attempting to compile it
diff --git a/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala b/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala
index c618f4760..848e2efd6 100644
--- a/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala
+++ b/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala
@@ -13,12 +13,12 @@ object ObpActorConfig {
val commonConf =
"""
- akka {
- loggers = ["akka.event.slf4j.Slf4jLogger"]
+ pekko {
+ loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"]
loglevel = """ + akka_loglevel + """
actor {
- provider = "akka.remote.RemoteActorRefProvider"
- allow-java-serialization = off
+ provider = "org.apache.pekko.remote.RemoteActorRefProvider"
+ allow-java-serialization = on
kryo {
type = "graph"
idstrategy = "default"
@@ -40,31 +40,31 @@ object ObpActorConfig {
resolve-subclasses = true
}
serializers {
- kryo = "com.twitter.chill.akka.AkkaSerializer"
+ java = "org.apache.pekko.serialization.JavaSerializer"
}
serialization-bindings {
- "net.liftweb.common.Full" = kryo,
- "net.liftweb.common.Empty" = kryo,
- "net.liftweb.common.Box" = kryo,
- "net.liftweb.common.ParamFailure" = kryo,
- "code.api.APIFailure" = kryo,
- "com.openbankproject.commons.model.BankAccount" = kryo,
- "com.openbankproject.commons.model.View" = kryo,
- "com.openbankproject.commons.model.User" = kryo,
- "com.openbankproject.commons.model.ViewId" = kryo,
- "com.openbankproject.commons.model.BankIdAccountIdViewId" = kryo,
- "com.openbankproject.commons.model.Permission" = kryo,
- "scala.Unit" = kryo,
- "scala.Boolean" = kryo,
- "java.io.Serializable" = kryo,
- "scala.collection.immutable.List" = kryo,
- "akka.actor.ActorSelectionMessage" = kryo,
- "code.model.Consumer" = kryo,
- "code.model.AppType" = kryo
+ "net.liftweb.common.Full" = java,
+ "net.liftweb.common.Empty" = java,
+ "net.liftweb.common.Box" = java,
+ "net.liftweb.common.ParamFailure" = java,
+ "code.api.APIFailure" = java,
+ "com.openbankproject.commons.model.BankAccount" = java,
+ "com.openbankproject.commons.model.View" = java,
+ "com.openbankproject.commons.model.User" = java,
+ "com.openbankproject.commons.model.ViewId" = java,
+ "com.openbankproject.commons.model.BankIdAccountIdViewId" = java,
+ "com.openbankproject.commons.model.Permission" = java,
+ "scala.Unit" = java,
+ "scala.Boolean" = java,
+ "java.io.Serializable" = java,
+ "scala.collection.immutable.List" = java,
+ "org.apache.pekko.actor.ActorSelectionMessage" = java,
+ "code.model.Consumer" = java,
+ "code.model.AppType" = java
}
}
remote {
- enabled-transports = ["akka.remote.netty.tcp"]
+ enabled-transports = ["org.apache.pekko.remote.netty.tcp"]
netty {
tcp {
send-buffer-size = 50000000
@@ -79,7 +79,7 @@ object ObpActorConfig {
val lookupConf =
s"""
${commonConf}
- akka {
+ pekko {
remote.netty.tcp.hostname = ${localHostname}
remote.netty.tcp.port = 0
}
@@ -88,7 +88,7 @@ object ObpActorConfig {
val localConf =
s"""
${commonConf}
- akka {
+ pekko {
remote.netty.tcp.hostname = ${localHostname}
remote.netty.tcp.port = ${localPort}
}
diff --git a/obp-api/src/main/scala/code/actorsystem/ObpActorSystem.scala b/obp-api/src/main/scala/code/actorsystem/ObpActorSystem.scala
index 6995e0af2..9189bd940 100644
--- a/obp-api/src/main/scala/code/actorsystem/ObpActorSystem.scala
+++ b/obp-api/src/main/scala/code/actorsystem/ObpActorSystem.scala
@@ -1,6 +1,6 @@
package code.actorsystem
-import akka.actor.ActorSystem
+import org.apache.pekko.actor.ActorSystem
import code.bankconnectors.akka.actor.AkkaConnectorActorConfig
import code.util.Helper
import code.util.Helper.MdcLoggable
diff --git a/obp-api/src/main/scala/code/actorsystem/ObpLookupSystem.scala b/obp-api/src/main/scala/code/actorsystem/ObpLookupSystem.scala
index a847b4f89..d9c9aeb83 100644
--- a/obp-api/src/main/scala/code/actorsystem/ObpLookupSystem.scala
+++ b/obp-api/src/main/scala/code/actorsystem/ObpLookupSystem.scala
@@ -1,12 +1,12 @@
package code.actorsystem
-import akka.actor.{ActorSystem}
+import org.apache.pekko.actor.{ActorSystem}
import code.api.util.APIUtil
import code.bankconnectors.LocalMappedOutInBoundTransfer
import code.bankconnectors.akka.actor.{AkkaConnectorActorConfig, AkkaConnectorHelperActor}
import code.util.Helper
import code.util.Helper.MdcLoggable
-import com.openbankproject.adapter.akka.commons.config.AkkaConfig
+// import com.openbankproject.adapter.pekko.commons.config.PekkoConfig // TODO: Re-enable when Pekko adapter is available
import com.typesafe.config.ConfigFactory
import net.liftweb.common.Full
@@ -38,7 +38,7 @@ trait ObpLookupSystem extends MdcLoggable {
if (port == 0) {
logger.error("Failed to connect to local Remotedata actor, the port is 0, can not find a proper port in current machine.")
}
- s"akka.tcp://ObpActorSystem_${props_hostname}@${hostname}:${port}/user/${actorName}"
+ s"pekko.tcp://ObpActorSystem_${props_hostname}@${hostname}:${port}/user/${actorName}"
}
this.obpLookupSystem.actorSelection(actorPath)
@@ -55,7 +55,7 @@ trait ObpLookupSystem extends MdcLoggable {
val hostname = h
val port = p
val akka_connector_hostname = Helper.getAkkaConnectorHostname
- s"akka.tcp://SouthSideAkkaConnector_${akka_connector_hostname}@${hostname}:${port}/user/${actorName}"
+ s"pekko.tcp://SouthSideAkkaConnector_${akka_connector_hostname}@${hostname}:${port}/user/${actorName}"
case _ =>
val hostname = AkkaConnectorActorConfig.localHostname
@@ -66,12 +66,12 @@ trait ObpLookupSystem extends MdcLoggable {
}
if(embeddedAdapter) {
- AkkaConfig(LocalMappedOutInBoundTransfer, Some(ObpActorSystem.northSideAkkaConnectorActorSystem))
+ // AkkaConfig(LocalMappedOutInBoundTransfer, Some(ObpActorSystem.northSideAkkaConnectorActorSystem)) // TODO: Re-enable when Pekko adapter is available
} else {
AkkaConnectorHelperActor.startAkkaConnectorHelperActors(ObpActorSystem.northSideAkkaConnectorActorSystem)
}
- s"akka.tcp://SouthSideAkkaConnector_${props_hostname}@${hostname}:${port}/user/${actorName}"
+ s"pekko.tcp://SouthSideAkkaConnector_${props_hostname}@${hostname}:${port}/user/${actorName}"
}
this.obpLookupSystem.actorSelection(actorPath)
}
diff --git a/obp-api/src/main/scala/code/api/dynamic/endpoint/helper/DynamicEndpointHelper.scala b/obp-api/src/main/scala/code/api/dynamic/endpoint/helper/DynamicEndpointHelper.scala
index 80f3b07d6..757ea0465 100644
--- a/obp-api/src/main/scala/code/api/dynamic/endpoint/helper/DynamicEndpointHelper.scala
+++ b/obp-api/src/main/scala/code/api/dynamic/endpoint/helper/DynamicEndpointHelper.scala
@@ -1,6 +1,6 @@
package code.api.dynamic.endpoint.helper
-import akka.http.scaladsl.model.{HttpMethods, HttpMethod => AkkaHttpMethod}
+import org.apache.pekko.http.scaladsl.model.{HttpMethods, HttpMethod => PekkoHttpMethod}
import code.DynamicData.{DynamicDataProvider, DynamicDataT}
import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT}
import code.api.util.APIUtil.{BigDecimalBody, BigIntBody, BooleanBody, DoubleBody, EmptyBody, FloatBody, IntBody, JArrayBody, LongBody, PrimaryDataBody, ResourceDoc, StringBody}
@@ -171,7 +171,7 @@ object DynamicEndpointHelper extends RestHelper {
* @param r HttpRequest
* @return (adapterUrl, requestBodyJson, httpMethod, requestParams, pathParams, role, operationId, mockResponseCode->mockResponseBody)
*/
- def unapply(r: Req): Option[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole, String, Option[(Int, JValue)], Option[String])] = {
+ def unapply(r: Req): Option[(String, JValue, PekkoHttpMethod, Map[String, List[String]], Map[String, String], ApiRole, String, Option[(Int, JValue)], Option[String])] = {
val requestUri = r.request.uri //eg: `/obp/dynamic-endpoint/fashion-brand-list/BRAND_ID`
val partPath = r.path.partPath //eg: List("fashion-brand-list","BRAND_ID"), the dynamic is from OBP URL, not in the partPath now.
@@ -179,7 +179,7 @@ object DynamicEndpointHelper extends RestHelper {
if (!testResponse_?(r) || !requestUri.startsWith(s"/${ApiStandards.obp.toString}/${ApiShortVersions.`dynamic-endpoint`.toString}"+urlPrefix))//if check the Content-Type contains json or not, and check the if it is the `dynamic_endpoints_url_prefix`
None //if do not match `URL and Content-Type`, then can not find this endpoint. return None.
else {
- val akkaHttpMethod = HttpMethods.getForKeyCaseInsensitive(r.requestType.method).get
+ val pekkoHttpMethod = HttpMethods.getForKeyCaseInsensitive(r.requestType.method).get
val httpMethod = HttpMethod.valueOf(r.requestType.method)
val urlQueryParameters = r.params
// url that match original swagger endpoint.
@@ -230,7 +230,7 @@ object DynamicEndpointHelper extends RestHelper {
val Some(role::_) = doc.roles
val requestBodyJValue = body(r).getOrElse(JNothing)
- Full(s"""$serverUrl$url""", requestBodyJValue, akkaHttpMethod, urlQueryParameters, pathParams, role, doc.operationId, mockResponse, bankId)
+ Full(s"""$serverUrl$url""", requestBodyJValue, pekkoHttpMethod, urlQueryParameters, pathParams, role, doc.operationId, mockResponse, bankId)
}
}
diff --git a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala
index dbe790ebb..df232a076 100644
--- a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala
+++ b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala
@@ -242,10 +242,10 @@ object DynamicUtil extends MdcLoggable{
|import java.util.Date
|import java.util.UUID.randomUUID
|
- |import _root_.akka.stream.StreamTcpException
- |import akka.http.scaladsl.model.headers.RawHeader
- |import akka.http.scaladsl.model.{HttpProtocol, _}
- |import akka.util.ByteString
+ |import _root_.org.apache.pekko.stream.StreamTcpException
+ |import org.apache.pekko.http.scaladsl.model.headers.RawHeader
+ |import org.apache.pekko.http.scaladsl.model.{HttpProtocol, _}
+ |import org.apache.pekko.util.ByteString
|import code.api.APIFailureNewStyle
|import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions
|import code.api.cache.Caching
diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala
index 2a538550d..80394c0c5 100644
--- a/obp-api/src/main/scala/code/api/util/NewStyle.scala
+++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala
@@ -1,7 +1,7 @@
package code.api.util
-import akka.http.scaladsl.model.HttpMethod
+import org.apache.pekko.http.scaladsl.model.HttpMethod
import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT}
import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID}
import code.api.builder.PaymentInitiationServicePISApi.APIMethods_PaymentInitiationServicePISApi.checkPaymentServerTypeError
diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
index c5b4a4738..f5ff01e40 100644
--- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
+++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
@@ -3194,20 +3194,87 @@ trait APIMethods510 {
"POST",
"/dynamic-registration/consumers",
"Create a Consumer(Dynamic Registration)",
- s"""Create a Consumer (mTLS access).
+ s"""Create a Consumer with full certificate validation (mTLS access) - **Recommended for PSD2/Berlin Group compliance**.
|
- | JWT payload:
- | - minimal
- | { "description":"Description" }
- | - full
- | {
- | "description": "Description",
- | "app_name": "Tesobe GmbH",
- | "app_type": "Sofit",
- | "developer_email": "marko@tesobe.com",
- | "redirect_url": "http://localhost:8082"
- | }
- | Please note that JWT must be signed with the counterpart private key of the public key used to establish mTLS
+ |This endpoint provides **secure, validated consumer registration** unlike the standard `/management/consumers` endpoint.
+ |
+ |**How it works (for comprehension flow):**
+ |
+ |1. **Extract JWT from request**: Parse the signed JWT from the request body
+ |2. **Extract certificate**: Get certificate from `PSD2-CERT` header in PEM format
+ |3. **Verify JWT signature**: Validate JWT is signed with the certificate's private key (proves possession)
+ |4. **Parse JWT payload**: Extract consumer details (description, app_name, app_type, developer_email, redirect_url)
+ |5. **Extract certificate info**: Parse certificate to get Common Name, Email, Organization
+ |6. **Validate against Regulated Entity**: Check certificate exists in Regulated Entity registry (PSD2 requirement)
+ |7. **Create consumer**: Generate credentials and create consumer record with validated certificate
+ |8. **Return consumer with certificate info**: Returns consumer details including parsed certificate information
+ |
+ |**Certificate Validation (CRITICAL SECURITY DIFFERENCE from regular creation):**
+ |
+ |[YES] **JWT Signature Verification**: JWT must be signed with certificate's private key - proves TPP owns the certificate
+ |[YES] **Regulated Entity Check**: Certificate must match a pre-registered Regulated Entity in the database
+ |[YES] **Certificate Binding**: Certificate is permanently bound to the consumer at creation time
+ |[YES] **CA Validation**: Certificate chain can be validated against trusted root CAs during API requests
+ |[YES] **PSD2 Compliance**: Meets EU regulatory requirements for TPP registration
+ |
+ |**Security benefits vs regular consumer creation:**
+ |
+ || Feature | Regular Creation | Dynamic Registration |
+ ||---------|-----------------|---------------------|
+ || Certificate validation | [NO] None | [YES] Full validation |
+ || Regulated Entity check | [NO] Not required | [YES] Required |
+ || JWT signature proof | [NO] Not required | [YES] Required (proves private key possession) |
+ || Self-signed certs | [YES] Accepted | [NO] Rejected |
+ || PSD2 compliant | [NO] No | [YES] Yes |
+ || Rogue TPP prevention | [NO] No | [YES] Yes |
+ |
+ |**Prerequisites:**
+ |1. TPP must be registered as a Regulated Entity with their certificate
+ |2. Certificate must be provided in `PSD2-CERT` request header (PEM format)
+ |3. JWT must be signed with the private key corresponding to the certificate
+ |4. Trust store must be configured with trusted root CAs
+ |
+ |**JWT Payload Structure:**
+ |
+ |Minimal:
+ |```json
+ |{ "description":"TPP Application Description" }
+ |```
+ |
+ |Full:
+ |```json
+ |{
+ | "description": "Payment Initiation Service",
+ | "app_name": "Tesobe GmbH",
+ | "app_type": "Confidential",
+ | "developer_email": "contact@tesobe.com",
+ | "redirect_url": "https://tpp.example.com/callback"
+ |}
+ |```
+ |
+ |**Note:** JWT must be signed with the private key that corresponds to the public key in the certificate sent via `PSD2-CERT` header.
+ |
+ |**Certificate Information Extraction:**
+ |
+ |The endpoint automatically extracts information from the certificate:
+ |- Common Name (CN) → used as app_name if not provided in JWT
+ |- Email Address → used as developer_email if not provided
+ |- Organization (O) → used as company
+ |- Certificate validity period
+ |- Issuer information
+ |
+ |**Configuration Required:**
+ |- `truststore.path.tpp_signature` - Path to trust store for CA validation
+ |- `truststore.password.tpp_signature` - Trust store password
+ |- Regulated Entity must be pre-registered with certificate public key
+ |
+ |**Error Scenarios:**
+ |- JWT signature invalid → `PostJsonIsNotSigned` (400)
+ |- Certificate not in Regulated Entity registry → `RegulatedEntityNotFoundByCertificate` (400)
+ |- Invalid JWT format → `InvalidJsonFormat` (400)
+ |- Missing PSD2-CERT header → Signature verification fails
+ |
+ |**This is the SECURE way to register consumers for production PSD2/Berlin Group implementations.**
|
|""",
ConsumerJwtPostJsonV510("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXNjcmlwdGlvbiI6IlRQUCBkZXNjcmlwdGlvbiJ9.c5gPPsyUmnVW774y7h2xyLXg0wdtu25nbU2AvOmyzcWa7JTdCKuuy3CblxueGwqYkQDDQIya1Qny4blyAvh_a1Q28LgzEKBcH7Em9FZXerhkvR9v4FWbCC5AgNLdQ7sR8-rUQdShmJcGDKdVmsZjuO4XhY2Zx0nFnkcvYfsU9bccoAvkKpVJATXzwBqdoEOuFlplnbxsMH1wWbAd3hbcPPWTdvO43xavNZTB5ybgrXVDEYjw8D-98_ZkqxS0vfvhJ4cGefHViaFzp6zXm7msdBpcE__O9rFbdl9Gvup_bsMbrHJioIrmc2d15Yc-tTNTF9J4qjD_lNxMRlx5o2TZEw"),
@@ -3283,6 +3350,85 @@ trait APIMethods510 {
"/management/consumers",
"Create a Consumer",
s"""Create a Consumer (Authenticated access).
+ |
+ |A Consumer represents an application that uses the Open Bank Project API. Each Consumer has:
+ |- A unique **key** (40 character random string) - used as the client ID for authentication
+ |- A unique **secret** (40 character random string) - used for secure authentication
+ |- An **app_type** (Confidential or Public) - determines OAuth2 flow requirements
+ |- Metadata like app_name, description, developer_email, company, etc.
+ |
+ |**How it works (for comprehension flow):**
+ |
+ |1. **Extract authenticated user**: Retrieves the currently logged-in user who is creating the consumer
+ |2. **Parse and validate JSON request**: Extracts the CreateConsumerRequestJsonV510 from the request body
+ |3. **Determine app_type**: Converts the string "Confidential" or "Public" to the AppType enum
+ |4. **Generate credentials**: Creates random 40-character key and secret for the new consumer
+ |5. **Create consumer record**: Calls createConsumerNewStyle with all parameters:
+ | - Auto-generated key and secret
+ | - enabled flag (controls if consumer is active)
+ | - app_name, description, developer_email, company
+ | - redirect_url (for OAuth flows)
+ | - client_certificate (optional, for certificate-based auth)
+ | - logo_url (optional)
+ | - createdByUserId (the authenticated user's ID)
+ |6. **Return response**: Returns the newly created consumer with HTTP 201 Created status
+ |
+ |**Client Certificate (Optional but Recommended for PSD2/Berlin Group):**
+ |
+ |The `client_certificate` field provides enhanced security through X.509 certificate validation.
+ |
+ |**IMPORTANT SECURITY NOTE:**
+ |- **This endpoint does NOT validate the certificate at creation time** - any certificate can be provided
+ |- The certificate is simply stored with the consumer record without checking if it's from a trusted CA
+ |- For PSD2/Berlin Group compliance with certificate validation, use the **Dynamic Registration** endpoint instead
+ |- Dynamic Registration validates certificates against registered Regulated Entities and trusted CAs
+ |
+ |**How certificates are used (after creation):**
+ |- Certificate is stored in PEM format (Base64-encoded X.509) with the consumer record
+ |- On subsequent API requests, the certificate from the `PSD2-CERT` header is compared against the stored certificate
+ |- If certificates don't match, access is denied even with valid OAuth2 tokens
+ |- First request populates the certificate if not set; subsequent requests must match that certificate
+ |
+ |**Certificate validation process (during API requests, NOT at consumer creation):**
+ |1. Certificate from `PSD2-CERT` header is compared to stored certificate (simple string match)
+ |2. Certificate is parsed from PEM format to X.509Certificate object
+ |3. Validated against a configured trust store (PKCS12 format) containing trusted root CAs
+ |4. Certificate chain is verified using PKIX validation
+ |5. Optional CRL (Certificate Revocation List) checking if enabled via `use_tpp_signature_revocation_list`
+ |6. Public key from certificate can verify signed requests (Berlin Group requirement)
+ |
+ |**Note:** Steps 3-6 only apply during API request validation, NOT during consumer creation via this endpoint.
+ |
+ |**Security benefits (when properly configured):**
+ |- **Certificate binding**: Links consumer to a specific certificate (prevents token reuse with different certs)
+ |- **Request verification**: Certificate's public key can verify signed requests
+ |- **Non-repudiation**: Certificate-based signatures prove request origin
+ |
+ |**Security limitations of this endpoint:**
+ |- **No validation at creation**: Any certificate (even self-signed or expired) can be stored
+ |- **No CA verification**: Certificate is not checked against trusted root CAs during creation
+ |- **No Regulated Entity check**: Does not verify the TPP is registered
+ |- **Use Dynamic Registration instead** for proper PSD2/Berlin Group compliance with full certificate validation
+ |
+ |**For proper PSD2 compliance:**
+ |Use the **Dynamic Consumer Registration** endpoint (`POST /obp/v5.1.0/dynamic-registration/consumers`) which:
+ |- Requires JWT-signed request using the certificate's private key
+ |- Validates certificate against Regulated Entity registry
+ |- Checks certificate is from a trusted CA using the configured trust store
+ |- Ensures proper QWAC/eIDAS compliance for EU TPPs
+ |
+ |**Configuration properties (for runtime validation):**
+ |- `truststore.path.tpp_signature` - Path to trust store for certificate validation during API requests
+ |- `truststore.password.tpp_signature` - Trust store password
+ |- `use_tpp_signature_revocation_list` - Enable/disable CRL checking during requests (default: true)
+ |- `consumer_validation_method_for_consent` - Set to "CONSUMER_CERTIFICATE" for cert-based validation
+ |- `bypass_tpp_signature_validation` - Emergency bypass (default: false, use only for testing)
+ |
+ |**Important**: The key and secret are only shown once in the response. Save them securely as they cannot be retrieved later.
+ |
+ |${consumerDisabledText()}
+ |
+ |${userAuthenticationMessage(true)}
|
|""",
createConsumerRequestJsonV510,
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
index e60580939..cea75c73b 100644
--- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
+++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
@@ -4152,20 +4152,23 @@ trait APIMethods600 {
|
|ABAC rules are Scala functions that return a Boolean value indicating whether access should be granted.
|
- |The rule function has the following signature:
- |```scala
- |(user: User, bankOpt: Option[Bank], accountOpt: Option[BankAccount], transactionOpt: Option[Transaction], customerOpt: Option[Customer]) => Boolean
- |```
+ |**Documentation:**
+ |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules
+ |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters
+ |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference
+ |- [ABAC Testing Examples](glossary#ABAC_Testing_Examples.md) - Testing examples and patterns
+ |
+ |The rule function receives 18 parameters including authenticatedUser, attributes, auth context, and optional objects (bank, account, transaction, etc.).
|
|Example rule code:
|```scala
- |// Allow access only if user email contains "admin"
- |user.emailAddress.contains("admin")
+ |// Allow access only if authenticated user is admin
+ |authenticatedUser.emailAddress.contains("admin")
|```
|
|```scala
|// Allow access only to accounts with balance > 1000
- |accountOpt.exists(_.balance.toString.toDouble > 1000.0)
+ |accountOpt.exists(_.balance.toDouble > 1000.0)
|```
|
|${userAuthenticationMessage(true)}
@@ -4242,6 +4245,11 @@ trait APIMethods600 {
"/management/abac-rules/ABAC_RULE_ID",
"Get ABAC Rule",
s"""Get an ABAC rule by its ID.
+ |
+ |**Documentation:**
+ |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules
+ |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters
+ |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference
|
|${userAuthenticationMessage(true)}
|
@@ -4290,6 +4298,11 @@ trait APIMethods600 {
"/management/abac-rules",
"Get ABAC Rules",
s"""Get all ABAC rules.
+ |
+ |**Documentation:**
+ |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules
+ |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters
+ |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference
|
|${userAuthenticationMessage(true)}
|
@@ -4340,6 +4353,11 @@ trait APIMethods600 {
"/management/abac-rules/ABAC_RULE_ID",
"Update ABAC Rule",
s"""Update an existing ABAC rule.
+ |
+ |**Documentation:**
+ |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules
+ |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters
+ |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference
|
|${userAuthenticationMessage(true)}
|
@@ -4413,7 +4431,11 @@ trait APIMethods600 {
"DELETE",
"/management/abac-rules/ABAC_RULE_ID",
"Delete ABAC Rule",
- s"""Delete an ABAC rule.
+ s"""Delete an ABAC rule by its ID.
+ |
+ |**Documentation:**
+ |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules
+ |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters
|
|${userAuthenticationMessage(true)}
|
@@ -4459,22 +4481,32 @@ trait APIMethods600 {
"Execute ABAC Rule",
s"""Execute an ABAC rule to test access control.
|
- |This endpoint allows you to test an ABAC rule with specific context (bank, account, transaction, customer).
+ |This endpoint allows you to test an ABAC rule with specific context (authenticated user, bank, account, transaction, customer, etc.).
+ |
+ |**Documentation:**
+ |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules
+ |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters
+ |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference
+ |- [ABAC Testing Examples](glossary#ABAC_Testing_Examples.md) - Testing examples and patterns
+ |
+ |You can provide optional IDs in the request body to test the rule with specific context.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
ExecuteAbacRuleJsonV600(
+ authenticated_user_id = None,
+ on_behalf_of_user_id = None,
+ user_id = None,
bank_id = Some("gh.29.uk"),
account_id = Some("8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"),
+ view_id = None,
transaction_id = None,
+ transaction_request_id = None,
customer_id = None
),
AbacRuleResultJsonV600(
- rule_id = "abc123",
- rule_name = "admin_only",
- result = true,
- message = "Access granted"
+ result = true
),
List(
UserNotLoggedIn,
@@ -4501,69 +4533,35 @@ trait APIMethods600 {
unboxFullOrFail(_, callContext, s"ABAC Rule not found with ID: $ruleId", 404)
}
- // Fetch context objects if IDs are provided
- bankOpt <- execJson.bank_id match {
- case Some(bankId) => NewStyle.function.getBank(BankId(bankId), callContext).map { case (bank, _) => Some(bank) }
- case None => Future.successful(None)
- }
+ // Execute the rule with IDs - object fetching happens internally
+ // authenticatedUserId: can be provided in request (for testing) or defaults to actual authenticated user
+ // onBehalfOfUserId: optional delegation - acting on behalf of another user
+ // userId: the target user being evaluated (defaults to authenticated user)
+ effectiveAuthenticatedUserId = execJson.authenticated_user_id.getOrElse(user.userId)
- accountOpt <- execJson.account_id match {
- case Some(accountId) if execJson.bank_id.isDefined =>
- NewStyle.function.getBankAccount(BankId(execJson.bank_id.get), AccountId(accountId), callContext)
- .map { case (account, _) => Some(account) }
- case _ => Future.successful(None)
- }
-
- transactionOpt <- execJson.transaction_id match {
- case Some(transId) if execJson.bank_id.isDefined && execJson.account_id.isDefined =>
- NewStyle.function.getTransaction(
- BankId(execJson.bank_id.get),
- AccountId(execJson.account_id.get),
- TransactionId(transId),
- callContext
- ).map { case (transaction, _) => Some(transaction) }.recover { case _ => None }
- case _ => Future.successful(None)
- }
-
- customerOpt <- execJson.customer_id match {
- case Some(custId) if execJson.bank_id.isDefined =>
- NewStyle.function.getCustomerByCustomerId(custId, callContext)
- .map { case (customer, _) => Some(customer) }.recover { case _ => None }
- case _ => Future.successful(None)
- }
-
- // Execute the rule
result <- Future {
- AbacRuleEngine.executeRule(
+ val resultBox = AbacRuleEngine.executeRule(
ruleId = ruleId,
- user = user,
- bankOpt = bankOpt,
- accountOpt = accountOpt,
- transactionOpt = transactionOpt,
- customerOpt = customerOpt
+ authenticatedUserId = effectiveAuthenticatedUserId,
+ onBehalfOfUserId = execJson.on_behalf_of_user_id,
+ userId = execJson.user_id,
+ callContext = callContext,
+ bankId = execJson.bank_id,
+ accountId = execJson.account_id,
+ viewId = execJson.view_id,
+ transactionId = execJson.transaction_id,
+ transactionRequestId = execJson.transaction_request_id,
+ customerId = execJson.customer_id
)
- } map {
- case Full(allowed) =>
- AbacRuleResultJsonV600(
- rule_id = ruleId,
- rule_name = rule.ruleName,
- result = allowed,
- message = if (allowed) "Access granted" else "Access denied"
- )
- case Failure(msg, _, _) =>
- AbacRuleResultJsonV600(
- rule_id = ruleId,
- rule_name = rule.ruleName,
- result = false,
- message = s"Execution error: $msg"
- )
- case Empty =>
- AbacRuleResultJsonV600(
- rule_id = ruleId,
- rule_name = rule.ruleName,
- result = false,
- message = "Execution failed"
- )
+
+ resultBox match {
+ case Full(allowed) =>
+ AbacRuleResultJsonV600(result = allowed)
+ case Failure(msg, _, _) =>
+ AbacRuleResultJsonV600(result = false)
+ case Empty =>
+ AbacRuleResultJsonV600(result = false)
+ }
}
} yield {
(result, HttpCode.`200`(callContext))
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
index aee2c3369..5af0070c2 100644
--- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
+++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
@@ -318,17 +318,19 @@ case class AbacRuleJsonV600(
case class AbacRulesJsonV600(abac_rules: List[AbacRuleJsonV600])
case class ExecuteAbacRuleJsonV600(
+ authenticated_user_id: Option[String],
+ on_behalf_of_user_id: Option[String],
+ user_id: Option[String],
bank_id: Option[String],
account_id: Option[String],
+ view_id: Option[String],
transaction_id: Option[String],
+ transaction_request_id: Option[String],
customer_id: Option[String]
)
case class AbacRuleResultJsonV600(
- rule_id: String,
- rule_name: String,
- result: Boolean,
- message: String
+ result: Boolean
)
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala
index cadaa87cb..4fe2e3b84 100644
--- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala
@@ -1,6 +1,6 @@
package code.bankconnectors
-import _root_.akka.http.scaladsl.model.HttpMethod
+import org.apache.pekko.http.scaladsl.model.HttpMethod
import code.api.attributedefinition.AttributeDefinition
import code.api.util.APIUtil.{OBPReturnType, _}
import code.api.util.ErrorMessages._
diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
index ba9d2db89..e92c6bdf5 100644
--- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
@@ -1,6 +1,6 @@
package code.bankconnectors
-import _root_.akka.http.scaladsl.model.HttpMethod
+import _root_.org.apache.pekko.http.scaladsl.model.HttpMethod
import code.DynamicData.DynamicDataProvider
import code.accountapplication.AccountApplicationX
import code.accountattribute.AccountAttributeX
diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala b/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala
index 61baf2bc3..0a182dc49 100644
--- a/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala
@@ -1,7 +1,7 @@
package code.bankconnectors.akka
import java.util.Date
-import akka.pattern.ask
+import org.apache.pekko.pattern.ask
import code.actorsystem.ObpLookupSystem
import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions
import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions.{bankAccountCommons, bankCommons, transaction, _}
diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala
index 5be16a13e..ca811607b 100644
--- a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala
@@ -16,12 +16,12 @@ object AkkaConnectorActorConfig {
val commonConf =
"""
- akka {
- loggers = ["akka.event.slf4j.Slf4jLogger"]
+ pekko {
+ loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"]
loglevel = """ + akka_loglevel + """
actor {
- provider = "akka.remote.RemoteActorRefProvider"
- allow-java-serialization = off
+ provider = "org.apache.pekko.remote.RemoteActorRefProvider"
+ allow-java-serialization = on
kryo {
type = "graph"
idstrategy = "default"
@@ -43,31 +43,31 @@ object AkkaConnectorActorConfig {
resolve-subclasses = true
}
serializers {
- kryo = "com.twitter.chill.akka.AkkaSerializer"
+ java = "org.apache.pekko.serialization.JavaSerializer"
}
serialization-bindings {
- "net.liftweb.common.Full" = kryo,
- "net.liftweb.common.Empty" = kryo,
- "net.liftweb.common.Box" = kryo,
- "net.liftweb.common.ParamFailure" = kryo,
- "code.api.APIFailure" = kryo,
- "com.openbankproject.commons.model.BankAccount" = kryo,
- "com.openbankproject.commons.model.View" = kryo,
- "com.openbankproject.commons.model.User" = kryo,
- "com.openbankproject.commons.model.ViewId" = kryo,
- "com.openbankproject.commons.model.BankIdAccountIdViewId" = kryo,
- "com.openbankproject.commons.model.Permission" = kryo,
- "scala.Unit" = kryo,
- "scala.Boolean" = kryo,
- "java.io.Serializable" = kryo,
- "scala.collection.immutable.List" = kryo,
- "akka.actor.ActorSelectionMessage" = kryo,
- "code.model.Consumer" = kryo,
- "code.model.AppType" = kryo
+ "net.liftweb.common.Full" = java,
+ "net.liftweb.common.Empty" = java,
+ "net.liftweb.common.Box" = java,
+ "net.liftweb.common.ParamFailure" = java,
+ "code.api.APIFailure" = java,
+ "com.openbankproject.commons.model.BankAccount" = java,
+ "com.openbankproject.commons.model.View" = java,
+ "com.openbankproject.commons.model.User" = java,
+ "com.openbankproject.commons.model.ViewId" = java,
+ "com.openbankproject.commons.model.BankIdAccountIdViewId" = java,
+ "com.openbankproject.commons.model.Permission" = java,
+ "scala.Unit" = java,
+ "scala.Boolean" = java,
+ "java.io.Serializable" = java,
+ "scala.collection.immutable.List" = java,
+ "org.apache.pekko.actor.ActorSelectionMessage" = java,
+ "code.model.Consumer" = java,
+ "code.model.AppType" = java
}
}
remote {
- enabled-transports = ["akka.remote.netty.tcp"]
+ enabled-transports = ["org.apache.pekko.remote.netty.tcp"]
netty {
tcp {
send-buffer-size = 50000000
@@ -82,7 +82,7 @@ object AkkaConnectorActorConfig {
val lookupConf =
s"""
${commonConf}
- akka {
+ pekko {
remote.netty.tcp.hostname = ${localHostname}
remote.netty.tcp.port = 0
}
@@ -91,7 +91,7 @@ object AkkaConnectorActorConfig {
val localConf =
s"""
${commonConf}
- akka {
+ pekko {
remote.netty.tcp.hostname = ${localHostname}
remote.netty.tcp.port = ${localPort}
}
@@ -100,7 +100,7 @@ object AkkaConnectorActorConfig {
val remoteConf =
s"""
${commonConf}
- akka {
+ pekko {
remote.netty.tcp.hostname = ${remoteHostname}
remote.netty.tcp.port = ${remotePort}
}
diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorInit.scala b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorInit.scala
index 0b8bc09ac..2170bf622 100644
--- a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorInit.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorInit.scala
@@ -1,6 +1,6 @@
package code.bankconnectors.akka.actor
-import akka.util.Timeout
+import org.apache.pekko.util.Timeout
import code.api.util.APIUtil
import code.util.Helper.MdcLoggable
diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorHelperActor.scala b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorHelperActor.scala
index b5c115bf3..f55d3e030 100644
--- a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorHelperActor.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorHelperActor.scala
@@ -1,6 +1,6 @@
package code.bankconnectors.akka.actor
-import akka.actor.{ActorSystem, Props}
+import org.apache.pekko.actor.{ActorSystem, Props}
import code.api.util.APIUtil
import code.util.Helper.MdcLoggable
diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/actor/SouthSideActorOfAkkaConnector.scala b/obp-api/src/main/scala/code/bankconnectors/akka/actor/SouthSideActorOfAkkaConnector.scala
index b9b9966d4..d06a3b375 100644
--- a/obp-api/src/main/scala/code/bankconnectors/akka/actor/SouthSideActorOfAkkaConnector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/akka/actor/SouthSideActorOfAkkaConnector.scala
@@ -1,6 +1,6 @@
package code.bankconnectors.akka.actor
-import akka.actor.{Actor, ActorLogging}
+import org.apache.pekko.actor.{Actor, ActorLogging}
import code.api.util.APIUtil.DateWithMsFormat
import code.api.util.ErrorMessages.attemptedToOpenAnEmptyBox
import code.api.util.{APIUtil, OBPFromDate, OBPLimit, OBPToDate}
diff --git a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala
index 01dbcc262..3247a16e5 100644
--- a/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/cardano/CardanoConnector_vJun2025.scala
@@ -83,7 +83,7 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable {
| $metadataJson
|}""".stripMargin
- request = prepareHttpRequest(paramUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend)
+ request = prepareHttpRequest(paramUrl, _root_.org.apache.pekko.http.scaladsl.model.HttpMethods.POST, _root_.org.apache.pekko.http.scaladsl.model.HttpProtocol("HTTP/1.1"), jsonToSend)
_ = logger.debug(s"CardanoConnector_vJun2025.makePaymentv210 request is : $request")
response <- NewStyle.function.tryons(s"${ErrorMessages.UnknownError} Failed to make HTTP request to Cardano API", 500, callContext) {
@@ -91,7 +91,7 @@ trait CardanoConnector_vJun2025 extends Connector with MdcLoggable {
}.flatten
responseBody <- NewStyle.function.tryons(s"${ErrorMessages.UnknownError} Failed to extract response body", 500, callContext) {
- response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String)
+ response.entity.dataBytes.runFold(_root_.org.apache.pekko.util.ByteString(""))(_ ++ _).map(_.utf8String)
}.flatten
_ <- Helper.booleanToFuture(s"${ErrorMessages.UnknownError} Cardano API returned error: ${response.status.value}", 500, callContext) {
diff --git a/obp-api/src/main/scala/code/bankconnectors/ethereum/EthereumConnector_vSept2025.scala b/obp-api/src/main/scala/code/bankconnectors/ethereum/EthereumConnector_vSept2025.scala
index 91fd4ee05..e8d819e25 100644
--- a/obp-api/src/main/scala/code/bankconnectors/ethereum/EthereumConnector_vSept2025.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/ethereum/EthereumConnector_vSept2025.scala
@@ -87,7 +87,7 @@ trait EthereumConnector_vSept2025 extends Connector with MdcLoggable {
}
for {
- request <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to build HTTP request", 500, callContext) {prepareHttpRequest(rpcUrl, _root_.akka.http.scaladsl.model.HttpMethods.POST, _root_.akka.http.scaladsl.model.HttpProtocol("HTTP/1.1"), payload)
+ request <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to build HTTP request", 500, callContext) {prepareHttpRequest(rpcUrl, _root_.org.apache.pekko.http.scaladsl.model.HttpMethods.POST, _root_.org.apache.pekko.http.scaladsl.model.HttpProtocol("HTTP/1.1"), payload)
}
response <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to call Ethereum RPC", 500, callContext) {
@@ -95,7 +95,7 @@ trait EthereumConnector_vSept2025 extends Connector with MdcLoggable {
}.flatten
body <- NewStyle.function.tryons(ErrorMessages.UnknownError + " Failed to read Ethereum RPC response", 500, callContext) {
- response.entity.dataBytes.runFold(_root_.akka.util.ByteString(""))(_ ++ _).map(_.utf8String)
+ response.entity.dataBytes.runFold(_root_.org.apache.pekko.util.ByteString(""))(_ ++ _).map(_.utf8String)
}.flatten
_ <- Helper.booleanToFuture(ErrorMessages.UnknownError + s" Ethereum RPC returned error: ${response.status.value}", 500, callContext) {
diff --git a/obp-api/src/main/scala/code/bankconnectors/package.scala b/obp-api/src/main/scala/code/bankconnectors/package.scala
index 78eed0f2f..e85f19c3f 100644
--- a/obp-api/src/main/scala/code/bankconnectors/package.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/package.scala
@@ -3,7 +3,7 @@ package code
import java.lang.reflect.Method
import java.util.regex.Pattern
-import akka.http.scaladsl.model.HttpMethod
+import org.apache.pekko.http.scaladsl.model.HttpMethod
import code.api.{APIFailureNewStyle, ApiVersionHolder}
import code.api.util.{CallContext, FutureUtil, NewStyle}
import code.methodrouting.{MethodRouting, MethodRoutingT}
diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala
index 53a3b7200..26304d01f 100644
--- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala
@@ -23,10 +23,10 @@ Osloerstrasse 16/17
Berlin 13359, Germany
*/
-import _root_.akka.stream.StreamTcpException
-import akka.http.scaladsl.model._
-import akka.http.scaladsl.model.headers.RawHeader
-import akka.util.ByteString
+import _root_.org.apache.pekko.stream.StreamTcpException
+import org.apache.pekko.http.scaladsl.model._
+import org.apache.pekko.http.scaladsl.model.headers.RawHeader
+import org.apache.pekko.util.ByteString
import code.api.APIFailureNewStyle
import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions
import code.api.dynamic.endpoint.helper.MockResponseHolder
diff --git a/obp-api/src/main/scala/code/customer/CustomerProvider.scala b/obp-api/src/main/scala/code/customer/CustomerProvider.scala
index 7c4bd205d..2f7952b1a 100644
--- a/obp-api/src/main/scala/code/customer/CustomerProvider.scala
+++ b/obp-api/src/main/scala/code/customer/CustomerProvider.scala
@@ -6,7 +6,7 @@ import code.api.util.{APIUtil, OBPQueryParam}
import com.openbankproject.commons.model.{User, _}
import net.liftweb.common.Box
import net.liftweb.util.SimpleInjector
-import akka.pattern.pipe
+import org.apache.pekko.pattern.pipe
import scala.collection.immutable.List
import scala.concurrent.Future
diff --git a/obp-api/src/main/scala/code/scheduler/DataBaseCleanerScheduler.scala b/obp-api/src/main/scala/code/scheduler/DataBaseCleanerScheduler.scala
index d2398e317..c72b08be8 100644
--- a/obp-api/src/main/scala/code/scheduler/DataBaseCleanerScheduler.scala
+++ b/obp-api/src/main/scala/code/scheduler/DataBaseCleanerScheduler.scala
@@ -1,6 +1,6 @@
package code.scheduler
-import code.actorsystem.ObpLookupSystem
+import code.actorsystem.ObpActorSystem
import code.api.Constant
import code.api.util.APIUtil.generateUUID
import code.api.util.APIUtil
@@ -17,7 +17,7 @@ import code.token.Tokens
object DataBaseCleanerScheduler extends MdcLoggable {
- private lazy val actorSystem = ObpLookupSystem.obpLookupSystem
+ private lazy val actorSystem = ObpActorSystem.localActorSystem
implicit lazy val executor = actorSystem.dispatcher
private lazy val scheduler = actorSystem.scheduler
private val oneDayInMillis: Long = 86400000
diff --git a/obp-api/src/main/scala/code/scheduler/DatabaseDriverScheduler.scala b/obp-api/src/main/scala/code/scheduler/DatabaseDriverScheduler.scala
index c31fe5086..1b9eeba61 100644
--- a/obp-api/src/main/scala/code/scheduler/DatabaseDriverScheduler.scala
+++ b/obp-api/src/main/scala/code/scheduler/DatabaseDriverScheduler.scala
@@ -3,7 +3,7 @@ package code.scheduler
import java.sql.SQLException
import java.util.concurrent.TimeUnit
-import code.actorsystem.ObpLookupSystem
+import code.actorsystem.ObpActorSystem
import code.util.Helper.MdcLoggable
import net.liftweb.db.{DB, SuperConnection}
@@ -12,7 +12,7 @@ import scala.concurrent.duration._
object DatabaseDriverScheduler extends MdcLoggable {
- private lazy val actorSystem = ObpLookupSystem.obpLookupSystem
+ private lazy val actorSystem = ObpActorSystem.localActorSystem
implicit lazy val executor = actorSystem.dispatcher
private lazy val scheduler = actorSystem.scheduler
diff --git a/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala b/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala
index 6c0eebb67..123397a0b 100644
--- a/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala
+++ b/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala
@@ -2,7 +2,7 @@ package code.scheduler
import java.util.concurrent.TimeUnit
import java.util.{Calendar, Date}
-import code.actorsystem.ObpLookupSystem
+import code.actorsystem.ObpActorSystem
import code.api.Constant
import code.api.util.APIUtil.generateUUID
import code.api.util.{APIUtil, OBPLimit, OBPToDate}
@@ -16,7 +16,7 @@ import scala.concurrent.duration._
object MetricsArchiveScheduler extends MdcLoggable {
- private lazy val actorSystem = ObpLookupSystem.obpLookupSystem
+ private lazy val actorSystem = ObpActorSystem.localActorSystem
implicit lazy val executor = actorSystem.dispatcher
private lazy val scheduler = actorSystem.scheduler
private val oneDayInMillis: Long = 86400000
diff --git a/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala b/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala
index 63fce1e1f..34772ab22 100644
--- a/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala
+++ b/obp-api/src/main/scala/code/scheduler/SchedulerUtil.scala
@@ -1,14 +1,14 @@
package code.scheduler
-import code.actorsystem.ObpLookupSystem
+import code.actorsystem.ObpActorSystem
import java.util.concurrent.TimeUnit
import java.util.{Calendar, Date}
import scala.concurrent.duration._
object SchedulerUtil {
- private lazy val actorSystem = ObpLookupSystem.obpLookupSystem
+ private lazy val actorSystem = ObpActorSystem.localActorSystem
implicit lazy val executor = actorSystem.dispatcher
private lazy val scheduler = actorSystem.scheduler
diff --git a/obp-api/src/main/scala/code/transactionstatus/TransactionRequestStatusScheduler.scala b/obp-api/src/main/scala/code/transactionstatus/TransactionRequestStatusScheduler.scala
index e3c7075db..3d8ed3b67 100644
--- a/obp-api/src/main/scala/code/transactionstatus/TransactionRequestStatusScheduler.scala
+++ b/obp-api/src/main/scala/code/transactionstatus/TransactionRequestStatusScheduler.scala
@@ -2,7 +2,7 @@ package code.transactionStatusScheduler
import java.util.concurrent.TimeUnit
-import code.actorsystem.ObpLookupSystem
+import code.actorsystem.ObpActorSystem
import code.transactionrequests.TransactionRequests
import code.util.Helper.MdcLoggable
@@ -11,7 +11,7 @@ import scala.concurrent.duration._
object TransactionRequestStatusScheduler extends MdcLoggable {
- private lazy val actorSystem = ObpLookupSystem.obpLookupSystem
+ private lazy val actorSystem = ObpActorSystem.localActorSystem
implicit lazy val executor = actorSystem.dispatcher
private lazy val scheduler = actorSystem.scheduler
diff --git a/obp-api/src/main/scala/code/util/AkkaHttpClient.scala b/obp-api/src/main/scala/code/util/AkkaHttpClient.scala
index 1438cd471..946c1a92b 100644
--- a/obp-api/src/main/scala/code/util/AkkaHttpClient.scala
+++ b/obp-api/src/main/scala/code/util/AkkaHttpClient.scala
@@ -1,10 +1,10 @@
package code.util
-import akka.http.scaladsl.Http
-import akka.http.scaladsl.model._
-import akka.http.scaladsl.settings.ConnectionPoolSettings
-import akka.stream.ActorMaterializer
+import org.apache.pekko.http.scaladsl.Http
+import org.apache.pekko.http.scaladsl.model._
+import org.apache.pekko.http.scaladsl.settings.ConnectionPoolSettings
+import org.apache.pekko.stream.ActorMaterializer
import code.actorsystem.ObpLookupSystem
import code.api.util.{APIUtil, CustomJsonFormats}
import code.util.Helper.MdcLoggable
diff --git a/obp-api/src/test/resources/frozen_type_meta_data b/obp-api/src/test/resources/frozen_type_meta_data
index 0ddfe92de..f120871b0 100644
Binary files a/obp-api/src/test/resources/frozen_type_meta_data and b/obp-api/src/test/resources/frozen_type_meta_data differ
diff --git a/obp-api/src/test/scala/code/api/v6_0_0/SystemViewsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/SystemViewsTest.scala
index 2891e33ba..d803e352f 100644
--- a/obp-api/src/test/scala/code/api/v6_0_0/SystemViewsTest.scala
+++ b/obp-api/src/test/scala/code/api/v6_0_0/SystemViewsTest.scala
@@ -71,7 +71,7 @@ class SystemViewsTest extends V600ServerSetup with DefaultUsers {
viewsArray.size should be > 0
And("Views should include system views like owner, accountant, auditor")
- val viewIds = viewsArray.map(view => (view \ "id").values.toString)
+ val viewIds = viewsArray.map(view => (view \ "view_id").values.toString)
viewIds should contain("owner")
}
}
@@ -137,7 +137,7 @@ class SystemViewsTest extends V600ServerSetup with DefaultUsers {
And("Response should contain the owner view details")
val json = response.body
- val viewId = (json \ "id").values.toString
+ val viewId = (json \ "view_id").values.toString
viewId should equal("owner")
And("View should be marked as system view")
@@ -159,7 +159,7 @@ class SystemViewsTest extends V600ServerSetup with DefaultUsers {
Then("We should get a 200 - Success")
responseAccountant.code should equal(200)
- val accountantViewId = (responseAccountant.body \ "id").values.toString
+ val accountantViewId = (responseAccountant.body \ "view_id").values.toString
accountantViewId should equal("accountant")
And("We request the auditor view")
@@ -168,7 +168,7 @@ class SystemViewsTest extends V600ServerSetup with DefaultUsers {
Then("We should get a 200 - Success")
responseAuditor.code should equal(200)
- val auditorViewId = (responseAuditor.body \ "id").values.toString
+ val auditorViewId = (responseAuditor.body \ "view_id").values.toString
auditorViewId should equal("auditor")
}
diff --git a/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala
index ed23f1b16..d1e900ec3 100644
--- a/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala
+++ b/obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala
@@ -34,6 +34,7 @@ import code.webuiprops.WebUiPropsCommons
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.ApiVersion
+import net.liftweb.json.JsonAST.JNothing
import net.liftweb.json.Serialization.write
import org.scalatest.Tag
@@ -335,7 +336,8 @@ class WebUiPropsTest extends V600ServerSetup {
val responseDelete = makeDeleteRequest(requestDelete)
Then("We should get a 204 No Content")
responseDelete.code should equal(204)
- responseDelete.body.toString should equal("{}")
+ // HTTP 204 No Content should have empty body
+ responseDelete.body shouldBe(JNothing)
}
scenario("DELETE WebUiProp - idempotent delete (delete twice)", VersionOfApi, ApiEndpoint3) {
diff --git a/obp-api/src/test/scala/code/setup/SendServerRequests.scala b/obp-api/src/test/scala/code/setup/SendServerRequests.scala
index e43c346cf..a3a8325df 100644
--- a/obp-api/src/test/scala/code/setup/SendServerRequests.scala
+++ b/obp-api/src/test/scala/code/setup/SendServerRequests.scala
@@ -180,7 +180,8 @@ trait SendServerRequests {
private def ApiResponseCommonPart(req: Req) = {
for (response <- Http.default(req > as.Response(p => p)))
yield {
- val body = if (response.getResponseBody().isEmpty) "{}" else response.getResponseBody()
+ //{} -->parse(body) => JObject(List()) , this is not "NO Content", change "" --> JNothing
+ val body = if (response.getResponseBody().isEmpty) "" else response.getResponseBody()
// Check that every response has a correlationId at Response Header
val list = response.getHeaders(ResponseHeader.`Correlation-Id`).asScala.toList
diff --git a/pom.xml b/pom.xml
index da476faed..f179b8559 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,11 +12,11 @@
2.12
2.12.20
- 2.5.32
+ 1.1.2
1.8.2
3.5.0
0.23.30
- 9.4.50.v20221201
+ 9.4.58.v20250814
2016.11-RC6-SNAPSHOT
UTF-8
diff --git a/release_notes.md b/release_notes.md
index e12e9de0a..f6b79cb2c 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -3,6 +3,33 @@
### Most recent changes at top of file
```
Date Commit Action
+12/12/2025 f2e7b827 Http4s runner configuration
+ Added http4s.host and http4s.port to props sample template:
+ - http4s.host=127.0.0.1
+ - http4s.port=8086
+ These properties control the bind address of bootstrap.http4s.Http4sServer
+ when running via the obp-http4s-runner fat JAR.
+11/12/2025 3c2df942 BREAKING CHANGE: Migration from Akka to Apache Pekko™ 1.1.2
+ Replaced Akka 2.5.32 with Apache Pekko™ 1.1.2 to address Akka licensing changes.
+ Updated all imports from com.typesafe.akka to org.apache.pekko.
+ Updated Jetty from 9.4.50 to 9.4.58 for improved Java 17 compatibility.
+
+ Migrated all actor systems to Apache Pekko™ and fixed critical scheduler
+ actor system initialization conflicts.
+ Consolidated all schedulers to use shared ObpActorSystem.localActorSystem.
+ Prevented multiple actor system creation during application boot.
+
+ Fixed actor system references in all schedulers:
+ - DataBaseCleanerScheduler
+ - DatabaseDriverScheduler
+ - MetricsArchiveScheduler
+ - SchedulerUtil
+ - TransactionRequestStatusScheduler
+
+ Resolved 'Address already in use' port binding errors.
+ Eliminated ExceptionInInitializerError during startup.
+ Fixed race conditions in actor system initialization.
+ All scheduler functionality preserved with improved stability.
TBD TBD Performance Improvement: Added caching to getProviders endpoint
Added configurable caching with memoization to GET /obp/v6.0.0/providers endpoint.
- Default cache TTL: 3600 seconds (1 hour)