mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:06:50 +00:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
fdf735477e
3
.gitignore
vendored
3
.gitignore
vendored
@ -40,4 +40,5 @@ marketing_diagram_generation/outputs/*
|
||||
.specstory
|
||||
project/project
|
||||
coursier
|
||||
metals.sbt
|
||||
metals.sbt
|
||||
obp-http4s-runner/src/main/resources/git.properties
|
||||
|
||||
11
README.md
11
README.md
@ -67,6 +67,17 @@ To compile and run Jetty, install Maven 3, create your configuration in `obp-api
|
||||
mvn install -pl .,obp-commons && mvn jetty:run -pl obp-api
|
||||
```
|
||||
|
||||
### Running http4s server (obp-http4s-runner)
|
||||
|
||||
To run the API using the http4s server (without Jetty), use the `obp-http4s-runner` module from the project root:
|
||||
|
||||
```sh
|
||||
MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G" mvn -pl obp-http4s-runner -am clean package -DskipTests=true -Dmaven.test.skip=true && \
|
||||
java -jar obp-http4s-runner/target/obp-http4s-runner.jar
|
||||
```
|
||||
|
||||
The http4s server binds to `http4s.host` / `http4s.port` as configured in your props file (defaults are `127.0.0.1` and `8181`).
|
||||
|
||||
### ZED IDE Setup
|
||||
|
||||
For ZED IDE users, we provide a complete development environment with Scala language server support:
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
354
obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md
Normal file
354
obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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._
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, _}
|
||||
|
||||
@ -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}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
4
pom.xml
4
pom.xml
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user