Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Marko Milić 2025-12-16 16:51:25 +01:00
commit fdf735477e
42 changed files with 2724 additions and 350 deletions

3
.gitignore vendored
View File

@ -40,4 +40,5 @@ marketing_diagram_generation/outputs/*
.specstory
project/project
coursier
metals.sbt
metals.sbt
obp-http4s-runner/src/main/resources/git.properties

View File

@ -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:

View File

@ -23,52 +23,6 @@
<webXmlPath>src/main/resources/web.xml</webXmlPath>
</properties>
</profile>
<profile>
<id>http4s-jar</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<finalName>${project.artifactId}-http4s</finalName>
<archive>
<manifest>
<mainClass>bootstrap.http4s.Http4sServer</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>true</unpack>
<scope>runtime</scope>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.build.outputDirectory}</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</configuration>
<executions>
<execution>
<id>http4s-fat-jar</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<pluginRepositories>
<pluginRepository>
@ -83,7 +37,8 @@
<groupId>com.tesobe</groupId>
<artifactId>obp-commons</artifactId>
</dependency>
<!--embed akka adapter start-->
<!--embed akka adapter start - COMMENTED OUT FOR PEKKO MIGRATION-->
<!-- TODO: Find or create Pekko equivalent for adapter-akka-commons
<dependency>
<groupId>com.github.OpenBankProject.OBP-Adapter-Akka-SpringBoot</groupId>
<artifactId>adapter-akka-commons</artifactId>
@ -95,6 +50,7 @@
</exclusion>
</exclusions>
</dependency>
-->
<!--embed akka adapter end-->
<dependency>
<groupId>com.github.everit-org.json-schema</groupId>
@ -288,21 +244,21 @@
<artifactId>signpost-commonshttp4</artifactId>
<version>1.2.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.typesafe.akka/akka-http-core -->
<!-- https://mvnrepository.com/artifact/org.apache.pekko/pekko-http-core -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-core_${scala.version}</artifactId>
<version>10.1.6</version>
<groupId>org.apache.pekko</groupId>
<artifactId>pekko-http-core_${scala.version}</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.version}</artifactId>
<version>${akka.version}</version>
<groupId>org.apache.pekko</groupId>
<artifactId>pekko-actor_${scala.version}</artifactId>
<version>${pekko.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-remote_${scala.version}</artifactId>
<version>${akka.version}</version>
<groupId>org.apache.pekko</groupId>
<artifactId>pekko-remote_${scala.version}</artifactId>
<version>${pekko.version}</version>
</dependency>
<dependency>
<groupId>com.sksamuel.avro4s</groupId>
@ -316,8 +272,8 @@
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>chill-akka_${scala.version}</artifactId>
<version>0.9.1</version>
<artifactId>chill_${scala.version}</artifactId>
<version>0.9.3</version>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
@ -337,9 +293,9 @@
<version>0.9.3</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_${scala.version}</artifactId>
<version>${akka.version}</version>
<groupId>org.apache.pekko</groupId>
<artifactId>pekko-slf4j_${scala.version}</artifactId>
<version>${pekko.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.dwickern/scala-nameof_2.11 -->
<dependency>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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))

View File

@ -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{

View File

@ -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._

View File

@ -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

View File

@ -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, _}

View File

@ -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}
}

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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) {

View File

@ -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) {

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")
}

View File

@ -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) {

View File

@ -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

View File

@ -12,11 +12,11 @@
<properties>
<scala.version>2.12</scala.version>
<scala.compiler>2.12.20</scala.compiler>
<akka.version>2.5.32</akka.version>
<pekko.version>1.1.2</pekko.version>
<avro.version>1.8.2</avro.version>
<lift.version>3.5.0</lift.version>
<http4s.version>0.23.30</http4s.version>
<jetty.version>9.4.50.v20221201</jetty.version>
<jetty.version>9.4.58.v20250814</jetty.version>
<obp-ri.version>2016.11-RC6-SNAPSHOT</obp-ri.version>
<!-- Common plugin settings -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -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)