Merge remote-tracking branch 'Marko/pekko' into develop

# Conflicts:
#	pom.xml
#	release_notes.md
This commit is contained in:
hongwei 2025-12-16 10:26:37 +01:00
commit 705de3070f
13 changed files with 2339 additions and 5 deletions

View File

@ -0,0 +1,477 @@
# HTML Pages Reference
## Overview
This document lists all HTML pages in the OBP-API application and their route mappings.
---
## Main Application Pages
### 1. Home & Landing Pages
#### index.html
- **Path:** `/index`
- **File:** `obp-api/src/main/webapp/index.html`
- **Route:** `Menu.i("Home") / "index"`
- **Authentication:** Not required
- **Purpose:** Main landing page for the API
#### index-en.html
- **Path:** `/index-en`
- **File:** `obp-api/src/main/webapp/index-en.html`
- **Route:** `Menu.i("index-en") / "index-en"`
- **Authentication:** Not required
- **Purpose:** English version of landing page
#### introduction.html
- **Path:** `/introduction`
- **File:** `obp-api/src/main/webapp/introduction.html`
- **Route:** `Menu.i("Introduction") / "introduction"`
- **Authentication:** Not required
- **Purpose:** Introduction to the API
---
## Authentication & User Management Pages
### 2. Login & User Information
#### already-logged-in.html
- **Path:** `/already-logged-in`
- **File:** `obp-api/src/main/webapp/already-logged-in.html`
- **Route:** `Menu("Already Logged In", "Already Logged In") / "already-logged-in"`
- **Authentication:** Not required
- **Purpose:** Shows message when user is already logged in
#### user-information.html
- **Path:** `/user-information`
- **File:** `obp-api/src/main/webapp/user-information.html`
- **Route:** `Menu("User Information", "User Information") / "user-information"`
- **Authentication:** Not required
- **Purpose:** Displays user information
### 3. Password Reset
#### Lost Password / Password Reset (Dynamically Generated)
- **Path:** `/user_mgt/lost_password` (lost password form)
- **Path:** `/user_mgt/reset_password/{TOKEN}` (reset password form)
- **File:** None (dynamically generated by Lift Framework)
- **Route:** Handled by `AuthUser.lostPassword` and `AuthUser.passwordReset` methods
- **Source:** `obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala`
- **Authentication:** Not required (public password reset)
- **Purpose:** Request and reset forgotten passwords
- **Note:** These are not static HTML files but are rendered by Lift's user management system
- **Links from:**
- `oauth/authorize.html` (line 30): "Forgotten password?" link
- `templates-hidden/_login.html` (line 31): "Forgotten password?" link
**API Endpoint for Password Reset URL:**
- **Path:** `POST /obp/v4.0.0/management/user/reset-password-url`
- **Role Required:** `CanCreateResetPasswordUrl`
- **Purpose:** Programmatically create password reset URLs
- **Property:** Controlled by `ResetPasswordUrlEnabled` (default: false)
### 4. User Invitation Pages
#### user-invitation.html
- **Path:** `/user-invitation`
- **File:** `obp-api/src/main/webapp/user-invitation.html`
- **Route:** `Menu("User Invitation", "User Invitation") / "user-invitation"`
- **Authentication:** Not required
- **Purpose:** User invitation form/page
#### user-invitation-info.html
- **Path:** `/user-invitation-info`
- **File:** `obp-api/src/main/webapp/user-invitation-info.html`
- **Route:** `Menu("User Invitation Info", "User Invitation Info") / "user-invitation-info"`
- **Authentication:** Not required
- **Purpose:** Information about user invitations
#### user-invitation-invalid.html
- **Path:** `/user-invitation-invalid`
- **File:** `obp-api/src/main/webapp/user-invitation-invalid.html`
- **Route:** `Menu("User Invitation Invalid", "User Invitation Invalid") / "user-invitation-invalid"`
- **Authentication:** Not required
- **Purpose:** Shows when invitation is invalid
#### user-invitation-warning.html
- **Path:** `/user-invitation-warning`
- **File:** `obp-api/src/main/webapp/user-invitation-warning.html`
- **Route:** `Menu("User Invitation Warning", "User Invitation Warning") / "user-invitation-warning"`
- **Authentication:** Not required
- **Purpose:** Shows warnings about invitations
---
## OAuth & Consent Pages
### 5. OAuth Flow Pages
#### oauth/authorize.html
- **Path:** `/oauth/authorize`
- **File:** `obp-api/src/main/webapp/oauth/authorize.html`
- **Route:** `Menu.i("OAuth") / "oauth" / "authorize"`
- **Authentication:** Not required (starts OAuth flow)
- **Purpose:** OAuth authorization page where users approve access
#### oauth/thanks.html
- **Path:** `/oauth/thanks` (via OAuthWorkedThanks.menu)
- **File:** `obp-api/src/main/webapp/oauth/thanks.html`
- **Route:** `OAuthWorkedThanks.menu`
- **Authentication:** Not required
- **Purpose:** OAuth completion page that performs redirect
### 6. Consent Management Pages
#### consent-screen.html
- **Path:** `/consent-screen`
- **File:** `obp-api/src/main/webapp/consent-screen.html`
- **Route:** `Menu("Consent Screen", Helper.i18n("consent.screen")) / "consent-screen" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** OAuth consent screen for approving permissions
#### consents.html
- **Path:** `/consents`
- **File:** `obp-api/src/main/webapp/consents.html`
- **Route:** `Menu.i("Consents") / "consents"`
- **Authentication:** Not required
- **Purpose:** View/manage consents
### 7. Berlin Group Consent Pages
#### confirm-bg-consent-request.html
- **Path:** `/confirm-bg-consent-request`
- **File:** `obp-api/src/main/webapp/confirm-bg-consent-request.html`
- **Route:** `Menu.i("confirm-bg-consent-request") / "confirm-bg-consent-request" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Berlin Group consent confirmation
#### confirm-bg-consent-request-sca.html
- **Path:** `/confirm-bg-consent-request-sca`
- **File:** `obp-api/src/main/webapp/confirm-bg-consent-request-sca.html`
- **Route:** `Menu.i("confirm-bg-consent-request-sca") / "confirm-bg-consent-request-sca" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Berlin Group consent with SCA (Strong Customer Authentication)
#### confirm-bg-consent-request-redirect-uri.html
- **Path:** `/confirm-bg-consent-request-redirect-uri`
- **File:** `obp-api/src/main/webapp/confirm-bg-consent-request-redirect-uri.html`
- **Route:** `Menu.i("confirm-bg-consent-request-redirect-uri") / "confirm-bg-consent-request-redirect-uri" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Berlin Group consent with redirect URI
### 8. VRP (Variable Recurring Payments) Consent Pages
#### confirm-vrp-consent-request.html
- **Path:** `/confirm-vrp-consent-request`
- **File:** `obp-api/src/main/webapp/confirm-vrp-consent-request.html`
- **Route:** `Menu.i("confirm-vrp-consent-request") / "confirm-vrp-consent-request" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** VRP consent request confirmation
#### confirm-vrp-consent.html
- **Path:** `/confirm-vrp-consent`
- **File:** `obp-api/src/main/webapp/confirm-vrp-consent.html`
- **Route:** `Menu.i("confirm-vrp-consent") / "confirm-vrp-consent" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** VRP consent confirmation
---
## Developer & Admin Pages
### 9. Consumer Management
#### consumer-registration.html
- **Path:** `/consumer-registration`
- **File:** `obp-api/src/main/webapp/consumer-registration.html`
- **Route:** `Menu("Consumer Registration", Helper.i18n("consumer.registration.nav.name")) / "consumer-registration" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Register new API consumers (OAuth applications)
### 10. Testing & Development
#### dummy-user-tokens.html
- **Path:** `/dummy-user-tokens`
- **File:** `obp-api/src/main/webapp/dummy-user-tokens.html`
- **Route:** `Menu("Dummy user tokens", "Get Dummy user tokens") / "dummy-user-tokens" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Get dummy user tokens for testing
#### create-sandbox-account.html
- **Path:** `/create-sandbox-account`
- **File:** `obp-api/src/main/webapp/create-sandbox-account.html`
- **Route:** `Menu("Sandbox Account Creation", "Create Bank Account") / "create-sandbox-account" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Create sandbox accounts for testing
- **Note:** Only available if `allow_sandbox_account_creation=true` in properties
---
## Security & Authentication Context Pages
### 11. User Authentication Context
#### add-user-auth-context-update-request.html
- **Path:** `/add-user-auth-context-update-request`
- **File:** `obp-api/src/main/webapp/add-user-auth-context-update-request.html`
- **Route:** `Menu.i("add-user-auth-context-update-request") / "add-user-auth-context-update-request"`
- **Authentication:** Not required
- **Purpose:** Add user authentication context update request
#### confirm-user-auth-context-update-request.html
- **Path:** `/confirm-user-auth-context-update-request`
- **File:** `obp-api/src/main/webapp/confirm-user-auth-context-update-request.html`
- **Route:** `Menu.i("confirm-user-auth-context-update-request") / "confirm-user-auth-context-update-request"`
- **Authentication:** Not required
- **Purpose:** Confirm user authentication context update
### 12. OTP (One-Time Password)
#### otp.html
- **Path:** `/otp`
- **File:** `obp-api/src/main/webapp/otp.html`
- **Route:** `Menu("Validate OTP", "Validate OTP") / "otp" >> AuthUser.loginFirst`
- **Authentication:** **Required** (AuthUser.loginFirst)
- **Purpose:** Validate one-time passwords
---
## Legal & Information Pages
### 13. Legal Pages
#### terms-and-conditions.html
- **Path:** `/terms-and-conditions`
- **File:** `obp-api/src/main/webapp/terms-and-conditions.html`
- **Route:** `Menu("Terms and Conditions", "Terms and Conditions") / "terms-and-conditions"`
- **Authentication:** Not required
- **Purpose:** Terms and conditions
#### privacy-policy.html
- **Path:** `/privacy-policy`
- **File:** `obp-api/src/main/webapp/privacy-policy.html`
- **Route:** `Menu("Privacy Policy", "Privacy Policy") / "privacy-policy"`
- **Authentication:** Not required
- **Purpose:** Privacy policy
---
## Documentation & Reference Pages
### 14. Documentation
#### sdks.html
- **Path:** `/sdks`
- **File:** `obp-api/src/main/webapp/sdks.html`
- **Route:** `Menu.i("SDKs") / "sdks"`
- **Authentication:** Not required
- **Purpose:** SDK documentation and downloads
#### static.html
- **Path:** `/static`
- **File:** `obp-api/src/main/webapp/static.html`
- **Route:** `Menu.i("Static") / "static"`
- **Authentication:** Not required
- **Purpose:** Static resource documentation
#### main-faq.html
- **Path:** Not directly routed (likely included/embedded)
- **File:** `obp-api/src/main/webapp/main-faq.html`
- **Route:** None (component file)
- **Authentication:** N/A
- **Purpose:** FAQ content
---
## Debug & Testing Pages
### 15. Debug Pages
#### debug.html
- **Path:** `/debug`
- **File:** `obp-api/src/main/webapp/debug.html`
- **Route:** `Menu.i("Debug") / "debug"`
- **Authentication:** Not required
- **Purpose:** Main debug page
#### debug/awake.html
- **Path:** `/debug/awake`
- **File:** `obp-api/src/main/webapp/debug/awake.html`
- **Route:** `Menu.i("awake") / "debug" / "awake"`
- **Authentication:** Not required
- **Purpose:** Test if API is running/responsive
#### debug/debug-basic.html
- **Path:** `/debug/debug-basic`
- **File:** `obp-api/src/main/webapp/debug/debug-basic.html`
- **Route:** `Menu.i("debug-basic") / "debug" / "debug-basic"`
- **Authentication:** Not required
- **Purpose:** Basic debug information
#### debug/debug-default-header.html
- **Path:** `/debug/debug-default-header`
- **File:** `obp-api/src/main/webapp/debug/debug-default-header.html`
- **Route:** `Menu.i("debug-default-header") / "debug" / "debug-default-header"`
- **Authentication:** Not required
- **Purpose:** Test default header template
#### debug/debug-default-footer.html
- **Path:** `/debug/debug-default-footer`
- **File:** `obp-api/src/main/webapp/debug/debug-default-footer.html`
- **Route:** `Menu.i("debug-default-footer") / "debug" / "debug-default-footer"`
- **Authentication:** Not required
- **Purpose:** Test default footer template
#### debug/debug-localization.html
- **Path:** `/debug/debug-localization`
- **File:** `obp-api/src/main/webapp/debug/debug-localization.html`
- **Route:** `Menu.i("debug-localization") / "debug" / "debug-localization"`
- **Authentication:** Not required
- **Purpose:** Test localization/i18n
#### debug/debug-plain.html
- **Path:** `/debug/debug-plain`
- **File:** `obp-api/src/main/webapp/debug/debug-plain.html`
- **Route:** `Menu.i("debug-plain") / "debug" / "debug-plain"`
- **Authentication:** Not required
- **Purpose:** Plain debug page without templates
#### debug/debug-webui.html
- **Path:** `/debug/debug-webui`
- **File:** `obp-api/src/main/webapp/debug/debug-webui.html`
- **Route:** `Menu.i("debug-webui") / "debug" / "debug-webui"`
- **Authentication:** Not required
- **Purpose:** Test WebUI properties
---
## Template Files (Not Directly Accessible)
### 16. Template Components
#### templates-hidden/_login.html
- **Path:** N/A (template component)
- **File:** `obp-api/src/main/webapp/templates-hidden/_login.html`
- **Route:** None (included by Lift framework)
- **Purpose:** Login form template component
- **Note:** Contains "Forgotten password?" link to `/user_mgt/lost_password`
#### templates-hidden/default.html
- **Path:** N/A (template)
- **File:** `obp-api/src/main/webapp/templates-hidden/default.html`
- **Route:** None (Lift framework template)
- **Purpose:** Default page template
#### templates-hidden/default-en.html
- **Path:** N/A (template)
- **File:** `obp-api/src/main/webapp/templates-hidden/default-en.html`
- **Route:** None (Lift framework template)
- **Purpose:** English default page template
#### templates-hidden/default-header.html
- **Path:** N/A (template)
- **File:** `obp-api/src/main/webapp/templates-hidden/default-header.html`
- **Route:** None (Lift framework template)
- **Purpose:** Default header template
#### templates-hidden/default-footer.html
- **Path:** N/A (template)
- **File:** `obp-api/src/main/webapp/templates-hidden/default-footer.html`
- **Route:** None (Lift framework template)
- **Purpose:** Default footer template
---
## Other Pages
### 17. Miscellaneous
#### basic.html
- **Path:** Not directly routed (likely used programmatically)
- **File:** `obp-api/src/main/webapp/basic.html`
- **Route:** None found
- **Purpose:** Basic HTML page template
---
## Route Configuration
All routes are defined in:
- **File:** `obp-api/src/main/scala/bootstrap/liftweb/Boot.scala`
- **Method:** `boot` method in `Boot` class
- **Framework:** Lift Web Framework's SiteMap
### Authentication Guards
- `>> AuthUser.loginFirst` - Requires user to be logged in
- `>> Admin.loginFirst` - Requires admin user to be logged in
- No guard - Public access
### Conditional Routes
Some routes are conditionally added based on properties:
- Sandbox account creation requires: `allow_sandbox_account_creation=true`
---
## URL Structure
All pages are served at:
```
https://[hostname]/[path]
```
For example:
- Home page: `https://api.example.com/index`
- OAuth: `https://api.example.com/oauth/authorize`
- Consent: `https://api.example.com/consent-screen`
---
## Summary Statistics
**Total HTML Files:** 43
- **Public Pages:** 27
- **Authenticated Pages:** 13
- **Template Components:** 5
- **Debug Pages:** 9
- **Dynamically Generated:** 2 (password reset pages)
**Page Categories:**
- Authentication & User Management: 7 pages
- Password Reset: 2 dynamically generated pages
- OAuth & Consent: 9 pages
- Developer & Admin: 3 pages
- Legal & Information: 4 pages
- Documentation: 4 pages
- Debug & Testing: 9 pages
- Templates: 5 files
- Miscellaneous: 2 pages
---
## Notes
1. **Lift Framework:** The application uses Lift Web Framework for routing and page rendering
2. **SiteMap:** Routes are configured via Lift's SiteMap in Boot.scala
3. **Templates:** Pages in `templates-hidden/` are not directly accessible but are used as layout templates
4. **Localization:** Some pages support internationalization (i18n) via `Helper.i18n()`
5. **Security:** Many pages require authentication via `AuthUser.loginFirst` or `Admin.loginFirst`
6. **OAuth Flow:** The OAuth authorization flow involves multiple pages: authorize → consent-screen → thanks
7. **Consent Types:** Different consent screens for different standards (Berlin Group, VRP, generic OAuth)
8. **Password Reset:** The password reset flow is handled dynamically by Lift's user management system, not static HTML files
- Lost password form: `/user_mgt/lost_password`
- Reset password form: `/user_mgt/reset_password/{TOKEN}`
- Implementation in: `code/model/dataAccess/AuthUser.scala`
---
## Related Files
- **Boot Configuration:** `obp-api/src/main/scala/bootstrap/liftweb/Boot.scala`
- **Menu Helpers:** Various classes in `code` package
- **Templates:** Lift framework `templates-hidden` directory
- **Static Resources:** JavaScript, CSS, and images in `webapp` directory
- **User Management:** `obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala` (password reset, validation)
- **Password Reset API:** `obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala` (resetPasswordUrl endpoint)

View File

@ -30,6 +30,7 @@ import code.CustomerDependants.MappedCustomerDependant
import code.DynamicData.DynamicData import code.DynamicData.DynamicData
import code.DynamicEndpoint.DynamicEndpoint import code.DynamicEndpoint.DynamicEndpoint
import code.UserRefreshes.MappedUserRefreshes import code.UserRefreshes.MappedUserRefreshes
import code.abacrule.MappedAbacRule
import code.accountapplication.MappedAccountApplication import code.accountapplication.MappedAccountApplication
import code.accountattribute.MappedAccountAttribute import code.accountattribute.MappedAccountAttribute
import code.accountholders.MapperAccountHolders import code.accountholders.MapperAccountHolders
@ -1040,6 +1041,7 @@ object ToSchemify {
MappedRegulatedEntity, MappedRegulatedEntity,
AtmAttribute, AtmAttribute,
Admin, Admin,
MappedAbacRule,
MappedBank, MappedBank,
MappedBankAccount, MappedBankAccount,
BankAccountRouting, BankAccountRouting,

View File

@ -0,0 +1,150 @@
package code.abacrule
import code.api.util.APIUtil
import com.openbankproject.commons.model._
import net.liftweb.common.Box
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo
import java.util.Date
trait AbacRule {
def abacRuleId: String
def ruleName: String
def ruleCode: String
def isActive: Boolean
def description: String
def createdByUserId: String
def updatedByUserId: String
}
class MappedAbacRule extends AbacRule with LongKeyedMapper[MappedAbacRule] with IdPK with CreatedUpdated {
def getSingleton = MappedAbacRule
object AbacRuleId extends MappedString(this, 255) {
override def defaultValue = APIUtil.generateUUID()
override def dbColumnName = "abac_rule_id"
}
object RuleName extends MappedString(this, 255) {
override def dbColumnName = "rule_name"
}
object RuleCode extends MappedText(this) {
override def dbColumnName = "rule_code"
}
object IsActive extends MappedBoolean(this) {
override def defaultValue = true
override def dbColumnName = "is_active"
}
object Description extends MappedText(this) {
override def dbColumnName = "description"
}
object CreatedByUserId extends MappedString(this, 255) {
override def dbColumnName = "created_by_user_id"
}
object UpdatedByUserId extends MappedString(this, 255) {
override def dbColumnName = "updated_by_user_id"
}
override def abacRuleId: String = AbacRuleId.get
override def ruleName: String = RuleName.get
override def ruleCode: String = RuleCode.get
override def isActive: Boolean = IsActive.get
override def description: String = Description.get
override def createdByUserId: String = CreatedByUserId.get
override def updatedByUserId: String = UpdatedByUserId.get
}
object MappedAbacRule extends MappedAbacRule with LongKeyedMetaMapper[MappedAbacRule] {
override def dbTableName = "abac_rule"
override def dbIndexes: List[BaseIndex[MappedAbacRule]] = Index(AbacRuleId) :: Index(RuleName) :: Index(CreatedByUserId) :: super.dbIndexes
}
trait AbacRuleProvider {
def getAbacRuleById(ruleId: String): Box[AbacRule]
def getAbacRuleByName(ruleName: String): Box[AbacRule]
def getAllAbacRules(): List[AbacRule]
def getActiveAbacRules(): List[AbacRule]
def createAbacRule(
ruleName: String,
ruleCode: String,
description: String,
isActive: Boolean,
createdBy: String
): Box[AbacRule]
def updateAbacRule(
ruleId: String,
ruleName: String,
ruleCode: String,
description: String,
isActive: Boolean,
updatedBy: String
): Box[AbacRule]
def deleteAbacRule(ruleId: String): Box[Boolean]
}
object MappedAbacRuleProvider extends AbacRuleProvider {
override def getAbacRuleById(ruleId: String): Box[AbacRule] = {
MappedAbacRule.find(By(MappedAbacRule.AbacRuleId, ruleId))
}
override def getAbacRuleByName(ruleName: String): Box[AbacRule] = {
MappedAbacRule.find(By(MappedAbacRule.RuleName, ruleName))
}
override def getAllAbacRules(): List[AbacRule] = {
MappedAbacRule.findAll()
}
override def getActiveAbacRules(): List[AbacRule] = {
MappedAbacRule.findAll(By(MappedAbacRule.IsActive, true))
}
override def createAbacRule(
ruleName: String,
ruleCode: String,
description: String,
isActive: Boolean,
createdBy: String
): Box[AbacRule] = {
tryo {
MappedAbacRule.create
.RuleName(ruleName)
.RuleCode(ruleCode)
.Description(description)
.IsActive(isActive)
.CreatedByUserId(createdBy)
.UpdatedByUserId(createdBy)
.saveMe()
}
}
override def updateAbacRule(
ruleId: String,
ruleName: String,
ruleCode: String,
description: String,
isActive: Boolean,
updatedBy: String
): Box[AbacRule] = {
for {
rule <- MappedAbacRule.find(By(MappedAbacRule.AbacRuleId, ruleId))
updatedRule <- tryo {
rule
.RuleName(ruleName)
.RuleCode(ruleCode)
.Description(description)
.IsActive(isActive)
.UpdatedByUserId(updatedBy)
.saveMe()
}
} yield updatedRule
}
override def deleteAbacRule(ruleId: String): Box[Boolean] = {
for {
rule <- MappedAbacRule.find(By(MappedAbacRule.AbacRuleId, ruleId))
deleted <- tryo(rule.delete_!)
} yield deleted
}
}

View File

@ -0,0 +1,229 @@
package code.abacrule
import code.api.util.{APIUtil, DynamicUtil}
import code.model.dataAccess.ResourceUser
import com.openbankproject.commons.model._
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
/**
* ABAC Rule Engine for compiling and executing Attribute-Based Access Control rules
*/
object AbacRuleEngine {
// Cache for compiled ABAC rule functions
private val compiledRulesCache: concurrent.Map[String, Box[AbacRuleFunction]] =
new ConcurrentHashMap[String, Box[AbacRuleFunction]]().asScala
/**
* Type alias for compiled ABAC rule function
* Parameters: User, Option[Bank], Option[Account], Option[Transaction], Option[Customer]
* Returns: Boolean (true = allow access, false = deny access)
*/
type AbacRuleFunction = (User, Option[Bank], Option[BankAccount], Option[Transaction], Option[Customer]) => Boolean
/**
* Compile an ABAC rule from Scala code
*
* @param ruleId Unique identifier for the rule
* @param ruleCode Scala code that defines the rule function
* @return Box containing the compiled function or error
*/
def compileRule(ruleId: String, ruleCode: String): Box[AbacRuleFunction] = {
compiledRulesCache.get(ruleId) match {
case Some(cachedFunction) => cachedFunction
case None =>
val compiledFunction = compileRuleInternal(ruleCode)
compiledRulesCache.put(ruleId, compiledFunction)
compiledFunction
}
}
/**
* Internal method to compile ABAC rule code
*/
private def compileRuleInternal(ruleCode: String): Box[AbacRuleFunction] = {
val fullCode = buildFullRuleCode(ruleCode)
DynamicUtil.compileScalaCode[AbacRuleFunction](fullCode) match {
case Full(func) => Full(func)
case Failure(msg, exception, _) =>
Failure(s"Failed to compile ABAC rule: $msg", exception, Empty)
case Empty =>
Failure("Failed to compile ABAC rule: Unknown error")
}
}
/**
* Build complete Scala code for compilation
*/
private def buildFullRuleCode(ruleCode: String): String = {
s"""
|import com.openbankproject.commons.model._
|import code.model.dataAccess.ResourceUser
|import net.liftweb.common._
|
|// ABAC Rule Function
|(user: User, bankOpt: Option[Bank], accountOpt: Option[BankAccount], transactionOpt: Option[Transaction], customerOpt: Option[Customer]) => {
| $ruleCode
|}
|""".stripMargin
}
/**
* Execute an ABAC rule
*
* @param ruleId The ID of the rule 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 allowed, Full(false) if denied, Failure on error
*/
def executeRule(
ruleId: String,
user: User,
bankOpt: Option[Bank] = None,
accountOpt: Option[BankAccount] = None,
transactionOpt: Option[Transaction] = None,
customerOpt: Option[Customer] = None
): Box[Boolean] = {
for {
rule <- MappedAbacRuleProvider.getAbacRuleById(ruleId)
_ <- 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)
}
} 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
*
* @param ruleCode The Scala code to validate
* @return Box[String] - Full("OK") if valid, Failure with error message if invalid
*/
def validateRuleCode(ruleCode: String): Box[String] = {
compileRuleInternal(ruleCode) match {
case Full(_) => Full("ABAC rule code is valid")
case Failure(msg, _, _) => Failure(s"Invalid ABAC rule code: $msg")
case Empty => Failure("Failed to validate ABAC rule code")
}
}
/**
* Clear the compiled rules cache
*/
def clearCache(): Unit = {
compiledRulesCache.clear()
}
/**
* Clear a specific rule from the cache
*/
def clearRuleFromCache(ruleId: String): Unit = {
compiledRulesCache.remove(ruleId)
}
/**
* Get cache statistics
*/
def getCacheStats(): Map[String, Any] = {
Map(
"cached_rules" -> compiledRulesCache.size,
"rule_ids" -> compiledRulesCache.keys.toList
)
}
}

View File

@ -0,0 +1,369 @@
package code.abacrule
/**
* ABAC Rule Examples
*
* This file contains example ABAC rules that can be used as templates.
* Copy the rule code (the string in quotes) when creating new ABAC rules via the API.
*/
object AbacRuleExamples {
// ==================== USER-BASED RULES ====================
/**
* Example 1: Admin Only Access
* Only users with "admin" in their email address can access
*/
val adminOnlyRule: String =
"""user.emailAddress.contains("admin")"""
/**
* Example 2: Specific User Provider
* Only allow users from a specific authentication provider
*/
val providerCheckRule: String =
"""user.provider == "obp""""
/**
* Example 3: User Email Domain
* Only allow users from specific email domain
*/
val emailDomainRule: String =
"""user.emailAddress.endsWith("@example.com")"""
/**
* Example 4: User Has Username
* Only allow users who have set a username
*/
val hasUsernameRule: String =
"""user.name.nonEmpty"""
// ==================== BANK-BASED RULES ====================
/**
* Example 5: Specific Bank Access
* Only allow access to a specific bank
*/
val specificBankRule: String =
"""bankOpt.exists(_.bankId.value == "gh.29.uk")"""
/**
* Example 6: Bank Short Name Check
* Only allow access to banks with specific short name
*/
val bankShortNameRule: String =
"""bankOpt.exists(_.shortName.contains("Example"))"""
/**
* Example 7: Bank Must Be Present
* Require bank context to be provided
*/
val bankRequiredRule: String =
"""bankOpt.isDefined"""
// ==================== ACCOUNT-BASED RULES ====================
/**
* Example 8: High Balance Accounts
* Only allow access to accounts with balance > 10,000
*/
val highBalanceRule: String =
"""accountOpt.exists(account => {
| account.balance.toString.toDoubleOption.exists(_ > 10000.0)
|})""".stripMargin
/**
* Example 9: Low Balance Accounts
* Only allow access to accounts with balance < 1,000
*/
val lowBalanceRule: String =
"""accountOpt.exists(account => {
| account.balance.toString.toDoubleOption.exists(_ < 1000.0)
|})""".stripMargin
/**
* Example 10: Specific Currency
* Only allow access to accounts with specific currency
*/
val currencyRule: String =
"""accountOpt.exists(_.currency == "EUR")"""
/**
* Example 11: Account Type Check
* Only allow access to savings accounts
*/
val accountTypeRule: String =
"""accountOpt.exists(_.accountType == "SAVINGS")"""
/**
* Example 12: Account Label Contains
* Only allow access to accounts with specific label
*/
val accountLabelRule: String =
"""accountOpt.exists(_.label.contains("VIP"))"""
// ==================== TRANSACTION-BASED RULES ====================
/**
* Example 13: Transaction Amount Limit
* Only allow access to transactions under 1,000
*/
val transactionLimitRule: String =
"""transactionOpt.exists(tx => {
| tx.amount.toString.toDoubleOption.exists(_ < 1000.0)
|})""".stripMargin
/**
* Example 14: Large Transactions Only
* Only allow access to transactions over 10,000
*/
val largeTransactionRule: String =
"""transactionOpt.exists(tx => {
| tx.amount.toString.toDoubleOption.exists(_ >= 10000.0)
|})""".stripMargin
/**
* Example 15: Specific Transaction Type
* Only allow access to specific transaction types
*/
val transactionTypeRule: String =
"""transactionOpt.exists(_.transactionType == "PAYMENT")"""
/**
* Example 16: Transaction Currency Check
* Only allow access to transactions in specific currency
*/
val transactionCurrencyRule: String =
"""transactionOpt.exists(_.currency == "USD")"""
// ==================== CUSTOMER-BASED RULES ====================
/**
* Example 17: Customer Email Domain
* Only allow access if customer email is from specific domain
*/
val customerEmailDomainRule: String =
"""customerOpt.exists(_.email.endsWith("@corporate.com"))"""
/**
* Example 18: Customer Legal Name Check
* Only allow access to customers with specific name pattern
*/
val customerNameRule: String =
"""customerOpt.exists(_.legalName.contains("Corporation"))"""
/**
* Example 19: Customer Mobile Number Pattern
* Only allow access to customers with specific mobile pattern
*/
val customerMobileRule: String =
"""customerOpt.exists(_.mobilePhoneNumber.startsWith("+44"))"""
// ==================== COMBINED RULES ====================
/**
* Example 20: Manager with Bank Context
* Managers can only access specific bank
*/
val managerBankRule: String =
"""user.emailAddress.contains("manager") &&
|bankOpt.exists(_.bankId.value == "gh.29.uk")""".stripMargin
/**
* Example 21: High Value Account Access
* Only managers can access high-value accounts
*/
val managerHighValueRule: String =
"""user.emailAddress.contains("manager") &&
|accountOpt.exists(account => {
| account.balance.toString.toDoubleOption.exists(_ > 50000.0)
|})""".stripMargin
/**
* Example 22: Auditor Transaction Access
* Auditors can only view completed transactions
*/
val auditorTransactionRule: String =
"""user.emailAddress.contains("auditor") &&
|transactionOpt.exists(_.status == "COMPLETED")""".stripMargin
/**
* Example 23: VIP Customer Manager Access
* Only specific managers can access VIP customer accounts
*/
val vipManagerRule: String =
"""(user.emailAddress.contains("vip-manager") || user.emailAddress.contains("director")) &&
|accountOpt.exists(_.label.contains("VIP"))""".stripMargin
/**
* Example 24: Multi-Condition Access
* Complex rule with multiple conditions
*/
val complexRule: String =
"""user.emailAddress.contains("manager") &&
|user.provider == "obp" &&
|bankOpt.exists(_.bankId.value == "gh.29.uk") &&
|accountOpt.exists(account => {
| account.currency == "GBP" &&
| account.balance.toString.toDoubleOption.exists(_ > 5000.0) &&
| account.balance.toString.toDoubleOption.exists(_ < 100000.0)
|})""".stripMargin
// ==================== NEGATIVE RULES (DENY ACCESS) ====================
/**
* Example 25: Block Specific User
* Deny access to specific user
*/
val blockUserRule: String =
"""!user.emailAddress.contains("blocked@example.com")"""
/**
* Example 26: Block Inactive Accounts
* Deny access to inactive accounts
*/
val blockInactiveAccountRule: String =
"""accountOpt.forall(_.accountRoutings.nonEmpty)"""
/**
* Example 27: Block Small Transactions
* Deny access to transactions under 10
*/
val blockSmallTransactionRule: String =
"""transactionOpt.forall(tx => {
| tx.amount.toString.toDoubleOption.exists(_ >= 10.0)
|})""".stripMargin
// ==================== ADVANCED RULES ====================
/**
* Example 28: Pattern Matching on User Email
* Use regex-like pattern matching
*/
val emailPatternRule: String =
"""user.emailAddress.matches(".*@(internal|corporate)\\.com")"""
/**
* Example 29: Multiple Bank Access
* Allow access to multiple specific banks
*/
val multipleBanksRule: String =
"""bankOpt.exists(bank => {
| val allowedBanks = Set("gh.29.uk", "de.10.de", "us.01.us")
| allowedBanks.contains(bank.bankId.value)
|})""".stripMargin
/**
* Example 30: Balance Range Check
* Only allow access to accounts within balance range
*/
val balanceRangeRule: String =
"""accountOpt.exists(account => {
| account.balance.toString.toDoubleOption.exists(balance =>
| balance >= 1000.0 && balance <= 50000.0
| )
|})""".stripMargin
/**
* Example 31: OR Logic - Multiple Valid Conditions
* Allow access if any condition is true
*/
val orLogicRule: String =
"""user.emailAddress.contains("admin") ||
|user.emailAddress.contains("manager") ||
|user.emailAddress.contains("director")""".stripMargin
/**
* Example 32: Nested Option Handling
* Safe navigation through optional values
*/
val nestedOptionRule: String =
"""bankOpt.isDefined &&
|accountOpt.isDefined &&
|accountOpt.exists(_.accountRoutings.nonEmpty)""".stripMargin
/**
* Example 33: Default to True (Allow All)
* Simple rule that always grants access (useful for testing)
*/
val allowAllRule: String = """true"""
/**
* Example 34: Default to False (Deny All)
* Simple rule that always denies access
*/
val denyAllRule: String = """false"""
/**
* Example 35: Context-Aware Rule
* Different logic based on what context is available
*/
val contextAwareRule: String =
"""if (transactionOpt.isDefined) {
| // If transaction context exists, apply transaction rules
| transactionOpt.exists(tx =>
| tx.amount.toString.toDoubleOption.exists(_ < 10000.0)
| )
|} else if (accountOpt.isDefined) {
| // If only account context exists, apply account rules
| accountOpt.exists(account =>
| account.balance.toString.toDoubleOption.exists(_ > 1000.0)
| )
|} else {
| // Default case
| user.emailAddress.contains("admin")
|}""".stripMargin
// ==================== HELPER FUNCTIONS ====================
/**
* Get all example rules as a map
*/
def getAllExamples: Map[String, String] = Map(
"admin_only" -> adminOnlyRule,
"provider_check" -> providerCheckRule,
"email_domain" -> emailDomainRule,
"has_username" -> hasUsernameRule,
"specific_bank" -> specificBankRule,
"bank_short_name" -> bankShortNameRule,
"bank_required" -> bankRequiredRule,
"high_balance" -> highBalanceRule,
"low_balance" -> lowBalanceRule,
"currency" -> currencyRule,
"account_type" -> accountTypeRule,
"account_label" -> accountLabelRule,
"transaction_limit" -> transactionLimitRule,
"large_transaction" -> largeTransactionRule,
"transaction_type" -> transactionTypeRule,
"transaction_currency" -> transactionCurrencyRule,
"customer_email_domain" -> customerEmailDomainRule,
"customer_name" -> customerNameRule,
"customer_mobile" -> customerMobileRule,
"manager_bank" -> managerBankRule,
"manager_high_value" -> managerHighValueRule,
"auditor_transaction" -> auditorTransactionRule,
"vip_manager" -> vipManagerRule,
"complex" -> complexRule,
"block_user" -> blockUserRule,
"block_inactive_account" -> blockInactiveAccountRule,
"block_small_transaction" -> blockSmallTransactionRule,
"email_pattern" -> emailPatternRule,
"multiple_banks" -> multipleBanksRule,
"balance_range" -> balanceRangeRule,
"or_logic" -> orLogicRule,
"nested_option" -> nestedOptionRule,
"allow_all" -> allowAllRule,
"deny_all" -> denyAllRule,
"context_aware" -> contextAwareRule
)
/**
* Get example by name
*/
def getExample(name: String): Option[String] = getAllExamples.get(name)
/**
* List all available example names
*/
def listExampleNames: List[String] = getAllExamples.keys.toList.sorted
}

View File

@ -0,0 +1,437 @@
# ABAC Rules Engine
## Overview
The ABAC (Attribute-Based Access Control) Rules Engine allows you to create, compile, and execute dynamic access control rules using Scala functions. This provides flexible, fine-grained access control based on attributes of users, banks, accounts, transactions, and customers.
## Architecture
### Components
1. **AbacRule** - Data model for storing ABAC rules
2. **AbacRuleProvider** - Provider interface for CRUD operations on rules
3. **AbacRuleEngine** - Compiler and executor for ABAC rules
4. **AbacRuleEndpoints** - REST API endpoints for managing and executing rules
### Rule Function Signature
Each ABAC rule is a Scala function with the following signature:
```scala
(
user: User,
bankOpt: Option[Bank],
accountOpt: Option[BankAccount],
transactionOpt: Option[Transaction],
customerOpt: Option[Customer]
) => Boolean
```
**Returns:**
- `true` - Access is granted
- `false` - Access is denied
## API Endpoints
All ABAC endpoints are under `/obp/v6.0.0/management/abac-rules` and require authentication.
### 1. Create ABAC Rule
**POST** `/management/abac-rules`
**Role Required:** `CanCreateAbacRule`
**Request Body:**
```json
{
"rule_name": "admin_only",
"rule_code": "user.emailAddress.contains(\"admin\")",
"description": "Only allow access to users with admin email",
"is_active": true
}
```
**Response:** (201 Created)
```json
{
"abac_rule_id": "abc123",
"rule_name": "admin_only",
"rule_code": "user.emailAddress.contains(\"admin\")",
"is_active": true,
"description": "Only allow access to users with admin email",
"created_by_user_id": "user123",
"updated_by_user_id": "user123"
}
```
### 2. Get ABAC Rule
**GET** `/management/abac-rules/{ABAC_RULE_ID}`
**Role Required:** `CanGetAbacRule`
### 3. Get All ABAC Rules
**GET** `/management/abac-rules`
**Role Required:** `CanGetAbacRule`
### 4. Update ABAC Rule
**PUT** `/management/abac-rules/{ABAC_RULE_ID}`
**Role Required:** `CanUpdateAbacRule`
### 5. Delete ABAC Rule
**DELETE** `/management/abac-rules/{ABAC_RULE_ID}`
**Role Required:** `CanDeleteAbacRule`
### 6. Execute ABAC Rule
**POST** `/management/abac-rules/{ABAC_RULE_ID}/execute`
**Role Required:** `CanExecuteAbacRule`
**Request Body:**
```json
{
"bank_id": "gh.29.uk",
"account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0",
"transaction_id": null,
"customer_id": null
}
```
**Response:**
```json
{
"rule_id": "abc123",
"rule_name": "admin_only",
"result": true,
"message": "Access granted"
}
```
## Rule Examples
### Example 1: Admin-Only Access
Only users with "admin" in their email can access:
```scala
user.emailAddress.contains("admin")
```
### Example 2: High Balance Accounts
Only allow access to accounts with balance > 10,000:
```scala
accountOpt.exists(account => {
account.balance.toString.toDoubleOption.exists(_ > 10000.0)
})
```
### Example 3: Specific Bank Access
Only allow access to a specific bank:
```scala
bankOpt.exists(_.bankId.value == "gh.29.uk")
```
### Example 4: Transaction Amount Limit
Only allow access to transactions under 1,000:
```scala
transactionOpt.exists(tx => {
tx.amount.toString.toDoubleOption.exists(_ < 1000.0)
})
```
### Example 5: Customer Email Domain
Only allow access if customer email is from a specific domain:
```scala
customerOpt.exists(_.email.endsWith("@example.com"))
```
### Example 6: Combined Rules
Multiple conditions combined:
```scala
user.emailAddress.contains("manager") &&
bankOpt.exists(_.bankId.value == "gh.29.uk") &&
accountOpt.exists(_.balance.toString.toDoubleOption.exists(_ > 5000.0))
```
### Example 7: User Provider Check
Only allow access from specific authentication provider:
```scala
user.provider == "obp" && user.emailAddress.nonEmpty
```
### Example 8: Time-Based Access (using Java time)
Access only during business hours (requires additional imports in the engine):
```scala
{
val hour = java.time.LocalTime.now().getHour
hour >= 9 && hour <= 17
}
```
## Programmatic Usage
### Compile a Rule
```scala
import code.abacrule.AbacRuleEngine
val ruleCode = """user.emailAddress.contains("admin")"""
val compiled = AbacRuleEngine.compileRule("rule123", ruleCode)
```
### Execute a Rule
```scala
import code.abacrule.AbacRuleEngine
import com.openbankproject.commons.model._
val result = AbacRuleEngine.executeRule(
ruleId = "rule123",
user = currentUser,
bankOpt = Some(bank),
accountOpt = Some(account),
transactionOpt = None,
customerOpt = None
)
result match {
case Full(true) => println("Access granted")
case Full(false) => println("Access denied")
case Failure(msg, _, _) => println(s"Error: $msg")
case Empty => println("Rule not found")
}
```
### Execute Multiple Rules (AND Logic)
All rules must pass:
```scala
val result = AbacRuleEngine.executeRulesAnd(
ruleIds = List("rule1", "rule2", "rule3"),
user = currentUser,
bankOpt = Some(bank)
)
```
### Execute Multiple Rules (OR Logic)
At least one rule must pass:
```scala
val result = AbacRuleEngine.executeRulesOr(
ruleIds = List("rule1", "rule2", "rule3"),
user = currentUser,
bankOpt = Some(bank)
)
```
### Validate Rule Code
```scala
val validation = AbacRuleEngine.validateRuleCode(ruleCode)
validation match {
case Full(msg) => println(s"Valid: $msg")
case Failure(msg, _, _) => println(s"Invalid: $msg")
case Empty => println("Validation failed")
}
```
### Cache Management
```scala
// Clear entire cache
AbacRuleEngine.clearCache()
// Clear specific rule
AbacRuleEngine.clearRuleFromCache("rule123")
// Get cache statistics
val stats = AbacRuleEngine.getCacheStats()
println(s"Cached rules: ${stats("cached_rules")}")
```
## Security Considerations
### Sandboxing
The ABAC engine can execute rules in a sandboxed environment with restricted permissions. Configure via:
```properties
dynamic_code_sandbox_permissions=[]
```
### Code Validation
All rule code is compiled before execution. Invalid Scala code will be rejected at creation/update time.
### Best Practices
1. **Test Rules Before Activating**: Use the execute endpoint to test rules with sample data
2. **Keep Rules Simple**: Complex logic is harder to debug and maintain
3. **Use Descriptive Names**: Name rules clearly to indicate their purpose
4. **Document Rules**: Use the description field to explain what the rule does
5. **Review Regularly**: Audit active rules periodically
6. **Version Control**: Keep rule code in version control alongside application code
7. **Fail-Safe**: Consider what happens if a rule fails - default to deny access
## Performance
### Compilation Caching
- Compiled rules are cached in memory
- Cache is automatically populated on first execution
- Cache is cleared when rules are updated or deleted
- Manual cache clearing available via `AbacRuleEngine.clearCache()`
### Execution Performance
- First execution: ~100-500ms (compilation + execution)
- Subsequent executions: ~1-10ms (cached execution)
## Database Schema
The `MappedAbacRule` table stores:
| Column | Type | Description |
|--------|------|-------------|
| id | Long | Primary key |
| mAbacRuleId | String(255) | Unique UUID |
| mRuleName | String(255) | Human-readable name |
| mRuleCode | Text | Scala function code |
| mIsActive | Boolean | Whether rule is active |
| mDescription | Text | Rule description |
| mCreatedByUserId | String(255) | User ID who created rule |
| mUpdatedByUserId | String(255) | User ID who last updated rule |
| createdAt | Timestamp | Creation timestamp |
| updatedAt | Timestamp | Last update timestamp |
Indexes:
- `mAbacRuleId` (unique)
- `mRuleName`
## Error Handling
### Common Errors
**Compilation Errors:**
```
Failed to compile ABAC rule: not found: value accountBalanc
```
→ Fix typos in rule code
**Runtime Errors:**
```
Execution error: java.lang.NullPointerException
```
→ Use safe navigation with `Option` types
**Inactive Rule:**
```
ABAC Rule admin_only is not active
```
→ Set `is_active: true` when creating/updating
### Safe Code Patterns
❌ **Unsafe:**
```scala
account.balance.toString.toDouble > 1000.0
```
✅ **Safe:**
```scala
accountOpt.exists(_.balance.toString.toDoubleOption.exists(_ > 1000.0))
```
## Integration Examples
### Protecting an Endpoint
```scala
// In your endpoint implementation
for {
(Full(user), callContext) <- authenticatedAccess(cc)
(bank, callContext) <- NewStyle.function.getBank(bankId, callContext)
(account, callContext) <- NewStyle.function.getBankAccount(bankId, accountId, callContext)
// Check ABAC rules
allowed <- Future {
AbacRuleEngine.executeRulesAnd(
ruleIds = List("bank_access_rule", "account_limit_rule"),
user = user,
bankOpt = Some(bank),
accountOpt = Some(account)
)
} map {
unboxFullOrFail(_, callContext, "ABAC access check failed", 403)
}
_ <- Helper.booleanToFuture(s"Access denied by ABAC rules", cc = callContext) {
allowed
}
// Continue with endpoint logic...
} yield {
// ...
}
```
## Roadmap
Future enhancements:
- [ ] Rule versioning
- [ ] Rule testing framework
- [ ] Rule analytics/logging
- [ ] Rule templates library
- [ ] Visual rule builder UI
- [ ] Rule impact analysis
- [ ] A/B testing for rules
- [ ] Rule scheduling (time-based activation)
- [ ] Rule dependencies/chaining
- [ ] Machine learning-based rule suggestions
## Technical Implementation Notes
### Lazy Initialization Pattern
The `AbacRuleEndpoints` trait uses lazy initialization to avoid `NullPointerException` during startup:
```scala
// Lazy initialization block - called when first endpoint is accessed
private lazy val abacResourceDocsRegistered: Boolean = {
registerAbacResourceDocs()
true
}
lazy val createAbacRule: OBPEndpoint = {
case "management" :: "abac-rules" :: Nil JsonPost json -> _ => {
abacResourceDocsRegistered // Triggers initialization
// ... endpoint implementation
}
}
```
**Why this is needed:**
- Traits are initialized before concrete classes
- `implementedInApiVersion` is provided by the mixing class
- Without lazy initialization, `ResourceDoc` creation would fail with null API version
- Lazy initialization ensures all values are set before first use
### Timestamp Fields
The `MappedAbacRule` class uses Lift's `CreatedUpdated` trait which automatically provides:
- `createdAt`: Timestamp when rule was created
- `updatedAt`: Timestamp when rule was last updated
These fields are:
- ✅ Stored in the database
- ✅ Automatically managed by Lift Mapper
- ❌ Not exposed in JSON responses (to keep API responses clean)
- ✅ Available internally for auditing
The JSON response only includes `created_by_user_id` and `updated_by_user_id` for tracking who modified the rule.
### Thread Safety
- **Rule Compilation**: Synchronized via ConcurrentHashMap
- **Cache Access**: Thread-safe through concurrent collections
- **Lazy Initialization**: Scala's lazy val is thread-safe by default
- **Database Access**: Handled by Lift Mapper's connection pooling
## Support
For issues or questions:
- Check the OBP API documentation
- Review existing rules in your deployment
- Test rules using the execute endpoint
- Check logs for compilation/execution errors
## License
Open Bank Project - AGPL v3

View File

@ -678,6 +678,21 @@ object ApiRole extends MdcLoggable{
case class CanGetViewPermissionsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole case class CanGetViewPermissionsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetViewPermissionsAtAllBanks = CanGetViewPermissionsAtAllBanks() lazy val canGetViewPermissionsAtAllBanks = CanGetViewPermissionsAtAllBanks()
case class CanCreateAbacRule(requiresBankId: Boolean = false) extends ApiRole
lazy val canCreateAbacRule = CanCreateAbacRule()
case class CanGetAbacRule(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetAbacRule = CanGetAbacRule()
case class CanUpdateAbacRule(requiresBankId: Boolean = false) extends ApiRole
lazy val canUpdateAbacRule = CanUpdateAbacRule()
case class CanDeleteAbacRule(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteAbacRule = CanDeleteAbacRule()
case class CanExecuteAbacRule(requiresBankId: Boolean = false) extends ApiRole
lazy val canExecuteAbacRule = CanExecuteAbacRule()
case class CanGetSystemLevelDynamicEntities(requiresBankId: Boolean = false) extends ApiRole case class CanGetSystemLevelDynamicEntities(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetSystemLevelDynamicEntities = CanGetSystemLevelDynamicEntities() lazy val canGetSystemLevelDynamicEntities = CanGetSystemLevelDynamicEntities()

View File

@ -37,6 +37,7 @@ object ApiTag {
val apiTagSystemView = ResourceDocTag("View-System") val apiTagSystemView = ResourceDocTag("View-System")
val apiTagEntitlement = ResourceDocTag("Entitlement") val apiTagEntitlement = ResourceDocTag("Entitlement")
val apiTagRole = ResourceDocTag("Role") val apiTagRole = ResourceDocTag("Role")
val apiTagABAC = ResourceDocTag("ABAC")
val apiTagScope = ResourceDocTag("Scope") val apiTagScope = ResourceDocTag("Scope")
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired") val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
val apiTagCounterparty = ResourceDocTag("Counterparty") val apiTagCounterparty = ResourceDocTag("Counterparty")

View File

@ -26,8 +26,10 @@ import code.api.v5_0_0.JSONFactory500
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500} import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510} import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo} import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson} import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CreateAbacRuleJsonV600, ExecuteAbacRuleJsonV600, UpdateAbacRuleJsonV600}
import code.api.v6_0_0.OBPAPI6_0_0 import code.api.v6_0_0.OBPAPI6_0_0
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
import code.metrics.APIMetrics import code.metrics.APIMetrics
import code.bankconnectors.LocalMappedConnectorInternal import code.bankconnectors.LocalMappedConnectorInternal
import code.bankconnectors.LocalMappedConnectorInternal._ import code.bankconnectors.LocalMappedConnectorInternal._
@ -74,12 +76,12 @@ trait APIMethods600 {
val Implementations6_0_0 = new Implementations600() val Implementations6_0_0 = new Implementations600()
class Implementations600 extends MdcLoggable { class Implementations600 extends RestHelper with MdcLoggable {
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0 val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0
private val staticResourceDocs = ArrayBuffer[ResourceDoc]() val staticResourceDocs = ArrayBuffer[ResourceDoc]()
def resourceDocs = staticResourceDocs val resourceDocs = staticResourceDocs
val apiRelations = ArrayBuffer[ApiRelation]() val apiRelations = ArrayBuffer[ApiRelation]()
val codeContext = CodeContext(staticResourceDocs, apiRelations) val codeContext = CodeContext(staticResourceDocs, apiRelations)
@ -4138,6 +4140,437 @@ trait APIMethods600 {
} }
} }
// ABAC Rule Endpoints
staticResourceDocs += ResourceDoc(
createAbacRule,
implementedInApiVersion,
nameOf(createAbacRule),
"POST",
"/management/abac-rules",
"Create ABAC Rule",
s"""Create a new ABAC (Attribute-Based Access Control) rule.
|
|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
|```
|
|Example rule code:
|```scala
|// Allow access only if user email contains "admin"
|user.emailAddress.contains("admin")
|```
|
|```scala
|// Allow access only to accounts with balance > 1000
|accountOpt.exists(_.balance.toString.toDouble > 1000.0)
|```
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
CreateAbacRuleJsonV600(
rule_name = "admin_only",
rule_code = """user.emailAddress.contains("admin")""",
description = "Only allow access to users with admin email",
is_active = true
),
AbacRuleJsonV600(
abac_rule_id = "abc123",
rule_name = "admin_only",
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
created_by_user_id = "user123",
updated_by_user_id = "user123"
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
InvalidJsonFormat,
UnknownError
),
List(apiTagABAC),
Some(List(canCreateAbacRule))
)
lazy val createAbacRule: OBPEndpoint = {
case "management" :: "abac-rules" :: Nil JsonPost json -> _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canCreateAbacRule, callContext)
createJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
json.extract[CreateAbacRuleJsonV600]
}
_ <- NewStyle.function.tryons(s"Rule name must not be empty", 400, callContext) {
createJson.rule_name.nonEmpty
}
_ <- NewStyle.function.tryons(s"Rule code must not be empty", 400, callContext) {
createJson.rule_code.nonEmpty
}
// Validate rule code by attempting to compile it
_ <- Future {
AbacRuleEngine.validateRuleCode(createJson.rule_code)
} map {
unboxFullOrFail(_, callContext, s"Invalid ABAC rule code", 400)
}
rule <- Future {
MappedAbacRuleProvider.createAbacRule(
ruleName = createJson.rule_name,
ruleCode = createJson.rule_code,
description = createJson.description,
isActive = createJson.is_active,
createdBy = user.userId
)
} map {
unboxFullOrFail(_, callContext, s"Could not create ABAC rule", 400)
}
} yield {
(createAbacRuleJsonV600(rule), HttpCode.`201`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
getAbacRule,
implementedInApiVersion,
nameOf(getAbacRule),
"GET",
"/management/abac-rules/ABAC_RULE_ID",
"Get ABAC Rule",
s"""Get an ABAC rule by its ID.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
AbacRuleJsonV600(
abac_rule_id = "abc123",
rule_name = "admin_only",
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
created_by_user_id = "user123",
updated_by_user_id = "user123"
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagABAC),
Some(List(canGetAbacRule))
)
lazy val getAbacRule: OBPEndpoint = {
case "management" :: "abac-rules" :: ruleId :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext)
rule <- Future {
MappedAbacRuleProvider.getAbacRuleById(ruleId)
} map {
unboxFullOrFail(_, callContext, s"ABAC Rule not found with ID: $ruleId", 404)
}
} yield {
(createAbacRuleJsonV600(rule), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
getAbacRules,
implementedInApiVersion,
nameOf(getAbacRules),
"GET",
"/management/abac-rules",
"Get ABAC Rules",
s"""Get all ABAC rules.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
AbacRulesJsonV600(
abac_rules = List(
AbacRuleJsonV600(
abac_rule_id = "abc123",
rule_name = "admin_only",
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
created_by_user_id = "user123",
updated_by_user_id = "user123"
)
)
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagABAC),
Some(List(canGetAbacRule))
)
lazy val getAbacRules: OBPEndpoint = {
case "management" :: "abac-rules" :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext)
rules <- Future {
MappedAbacRuleProvider.getAllAbacRules()
}
} yield {
(createAbacRulesJsonV600(rules), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
updateAbacRule,
implementedInApiVersion,
nameOf(updateAbacRule),
"PUT",
"/management/abac-rules/ABAC_RULE_ID",
"Update ABAC Rule",
s"""Update an existing ABAC rule.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
UpdateAbacRuleJsonV600(
rule_name = "admin_only_updated",
rule_code = """user.emailAddress.contains("admin") && user.provider == "obp"""",
description = "Only allow access to OBP admin users",
is_active = true
),
AbacRuleJsonV600(
abac_rule_id = "abc123",
rule_name = "admin_only_updated",
rule_code = """user.emailAddress.contains("admin") && user.provider == "obp"""",
is_active = true,
description = "Only allow access to OBP admin users",
created_by_user_id = "user123",
updated_by_user_id = "user456"
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
InvalidJsonFormat,
UnknownError
),
List(apiTagABAC),
Some(List(canUpdateAbacRule))
)
lazy val updateAbacRule: OBPEndpoint = {
case "management" :: "abac-rules" :: ruleId :: Nil JsonPut json -> _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canUpdateAbacRule, callContext)
updateJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
json.extract[UpdateAbacRuleJsonV600]
}
// Validate rule code by attempting to compile it
_ <- Future {
AbacRuleEngine.validateRuleCode(updateJson.rule_code)
} map {
unboxFullOrFail(_, callContext, s"Invalid ABAC rule code", 400)
}
rule <- Future {
MappedAbacRuleProvider.updateAbacRule(
ruleId = ruleId,
ruleName = updateJson.rule_name,
ruleCode = updateJson.rule_code,
description = updateJson.description,
isActive = updateJson.is_active,
updatedBy = user.userId
)
} map {
unboxFullOrFail(_, callContext, s"Could not update ABAC rule with ID: $ruleId", 400)
}
// Clear rule from cache after update
_ <- Future {
AbacRuleEngine.clearRuleFromCache(ruleId)
}
} yield {
(createAbacRuleJsonV600(rule), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
deleteAbacRule,
implementedInApiVersion,
nameOf(deleteAbacRule),
"DELETE",
"/management/abac-rules/ABAC_RULE_ID",
"Delete ABAC Rule",
s"""Delete an ABAC rule.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
EmptyBody,
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagABAC),
Some(List(canDeleteAbacRule))
)
lazy val deleteAbacRule: OBPEndpoint = {
case "management" :: "abac-rules" :: ruleId :: Nil JsonDelete _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canDeleteAbacRule, callContext)
deleted <- Future {
MappedAbacRuleProvider.deleteAbacRule(ruleId)
} map {
unboxFullOrFail(_, callContext, s"Could not delete ABAC rule with ID: $ruleId", 400)
}
// Clear rule from cache after deletion
_ <- Future {
AbacRuleEngine.clearRuleFromCache(ruleId)
}
} yield {
(Full(deleted), HttpCode.`204`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
executeAbacRule,
implementedInApiVersion,
nameOf(executeAbacRule),
"POST",
"/management/abac-rules/ABAC_RULE_ID/execute",
"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).
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
ExecuteAbacRuleJsonV600(
bank_id = Some("gh.29.uk"),
account_id = Some("8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"),
transaction_id = None,
customer_id = None
),
AbacRuleResultJsonV600(
rule_id = "abc123",
rule_name = "admin_only",
result = true,
message = "Access granted"
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
InvalidJsonFormat,
UnknownError
),
List(apiTagABAC),
Some(List(canExecuteAbacRule))
)
lazy val executeAbacRule: OBPEndpoint = {
case "management" :: "abac-rules" :: ruleId :: "execute" :: Nil JsonPost json -> _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canExecuteAbacRule, callContext)
execJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
json.extract[ExecuteAbacRuleJsonV600]
}
rule <- Future {
MappedAbacRuleProvider.getAbacRuleById(ruleId)
} map {
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)
}
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(
ruleId = ruleId,
user = user,
bankOpt = bankOpt,
accountOpt = accountOpt,
transactionOpt = transactionOpt,
customerOpt = customerOpt
)
} 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"
)
}
} yield {
(result, HttpCode.`200`(callContext))
}
}
}
} }
} }

View File

@ -290,6 +290,47 @@ case class CustomerWithAttributesJsonV600(
customer_attributes: List[CustomerAttributeResponseJsonV300] customer_attributes: List[CustomerAttributeResponseJsonV300]
) )
// ABAC Rule JSON models
case class CreateAbacRuleJsonV600(
rule_name: String,
rule_code: String,
description: String,
is_active: Boolean
)
case class UpdateAbacRuleJsonV600(
rule_name: String,
rule_code: String,
description: String,
is_active: Boolean
)
case class AbacRuleJsonV600(
abac_rule_id: String,
rule_name: String,
rule_code: String,
is_active: Boolean,
description: String,
created_by_user_id: String,
updated_by_user_id: String
)
case class AbacRulesJsonV600(abac_rules: List[AbacRuleJsonV600])
case class ExecuteAbacRuleJsonV600(
bank_id: Option[String],
account_id: Option[String],
transaction_id: Option[String],
customer_id: Option[String]
)
case class AbacRuleResultJsonV600(
rule_id: String,
rule_name: String,
result: Boolean,
message: String
)
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{ object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = { def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = {
@ -735,4 +776,20 @@ case class UpdateViewJsonV600(
def createViewsJsonV600(views: List[View]): ViewsJsonV600 = { def createViewsJsonV600(views: List[View]): ViewsJsonV600 = {
ViewsJsonV600(views.map(createViewJsonV600)) ViewsJsonV600(views.map(createViewJsonV600))
} }
def createAbacRuleJsonV600(rule: code.abacrule.AbacRule): AbacRuleJsonV600 = {
AbacRuleJsonV600(
abac_rule_id = rule.abacRuleId,
rule_name = rule.ruleName,
rule_code = rule.ruleCode,
is_active = rule.isActive,
description = rule.description,
created_by_user_id = rule.createdByUserId,
updated_by_user_id = rule.updatedByUserId
)
}
def createAbacRulesJsonV600(rules: List[code.abacrule.AbacRule]): AbacRulesJsonV600 = {
AbacRulesJsonV600(rules.map(createAbacRuleJsonV600))
}
} }

View File

@ -0,0 +1,143 @@
package code.api.v6_0_0
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole.CanGetViewPermissionsAtAllBanks
import code.api.util.ErrorMessages
import code.api.util.ErrorMessages.UserHasMissingRoles
import code.api.v6_0_0.APIMethods600.Implementations6_0_0
import code.entitlement.Entitlement
import code.setup.DefaultUsers
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model.ErrorMessage
import com.openbankproject.commons.util.ApiVersion
import org.scalatest.Tag
class ViewPermissionsTest extends V600ServerSetup with DefaultUsers {
override def beforeAll(): Unit = {
super.beforeAll()
}
override def afterAll(): Unit = {
super.afterAll()
}
/**
* Test tags
* Example: To run tests with tag "getViewPermissions":
* mvn test -D tagsToInclude
*
* This is made possible by the scalatest maven plugin
*/
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.getViewPermissions))
feature(s"Test GET /management/view-permissions endpoint - $VersionOfApi") {
scenario("We try to get view permissions - Anonymous access", ApiEndpoint1, VersionOfApi) {
When("We make the request without authentication")
val request = (v6_0_0_Request / "management" / "view-permissions").GET
val response = makeGetRequest(request)
Then("We should get a 401 - User Not Logged In")
response.code should equal(401)
response.body.extract[ErrorMessage].message should equal(ErrorMessages.UserNotLoggedIn)
}
scenario("We try to get view permissions without proper role - Authorized access", ApiEndpoint1, VersionOfApi) {
When("We make the request as user1 without the CanGetViewPermissionsAtAllBanks role")
val request = (v6_0_0_Request / "management" / "view-permissions").GET <@ (user1)
val response = makeGetRequest(request)
Then("We should get a 403 - Missing Required Role")
response.code should equal(403)
And("Error message should indicate missing CanGetViewPermissionsAtAllBanks role")
response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanGetViewPermissionsAtAllBanks)
}
scenario("We try to get view permissions with proper role - Authorized access", ApiEndpoint1, VersionOfApi) {
When("We grant the CanGetViewPermissionsAtAllBanks role to user1")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetViewPermissionsAtAllBanks.toString)
And("We make the request as user1 with the CanGetViewPermissionsAtAllBanks role")
val request = (v6_0_0_Request / "management" / "view-permissions").GET <@ (user1)
val response = makeGetRequest(request)
Then("We should get a 200 - Success")
response.code should equal(200)
And("Response should contain a permissions array")
val json = response.body
val permissionsArray = (json \ "permissions").children
permissionsArray.size should be > 0
And("Each permission should have permission and category fields")
permissionsArray.foreach { permission =>
(permission \ "permission").values.toString should not be empty
(permission \ "category").values.toString should not be empty
}
And("Permissions should include standard view permissions")
val permissionNames = permissionsArray.map(p => (p \ "permission").values.toString)
permissionNames should contain("can_see_transaction_amount")
permissionNames should contain("can_see_bank_account_balance")
permissionNames should contain("can_create_custom_view")
permissionNames should contain("can_grant_access_to_views")
And("Permissions should have appropriate categories")
val categories = permissionsArray.map(p => (p \ "category").values.toString).distinct
categories.size should be > 0
}
scenario("Verify all permission constants are included", ApiEndpoint1, VersionOfApi) {
When("We grant the CanGetViewPermissionsAtAllBanks role to user1")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetViewPermissionsAtAllBanks.toString)
And("We make the request as user1")
val request = (v6_0_0_Request / "management" / "view-permissions").GET <@ (user1)
val response = makeGetRequest(request)
Then("Response should include all key permissions")
val json = response.body
val permissionNames = (json \ "permissions").children.map(p => (p \ "permission").values.toString)
// Transaction permissions
permissionNames should contain("can_see_transaction_this_bank_account")
permissionNames should contain("can_see_transaction_other_bank_account")
permissionNames should contain("can_see_transaction_metadata")
permissionNames should contain("can_see_transaction_description")
// Account permissions
permissionNames should contain("can_see_bank_account_owners")
permissionNames should contain("can_see_bank_account_iban")
permissionNames should contain("can_see_bank_account_number")
permissionNames should contain("can_update_bank_account_label")
// Counterparty permissions
permissionNames should contain("can_see_other_account_iban")
permissionNames should contain("can_add_counterparty")
permissionNames should contain("can_delete_counterparty")
// Metadata permissions
permissionNames should contain("can_see_comments")
permissionNames should contain("can_add_comment")
permissionNames should contain("can_see_tags")
permissionNames should contain("can_add_tag")
// Transaction Request permissions
permissionNames should contain("can_add_transaction_request_to_own_account")
permissionNames should contain("can_add_transaction_request_to_any_account")
permissionNames should contain("can_see_transaction_requests")
// View Management permissions
permissionNames should contain("can_create_custom_view")
permissionNames should contain("can_delete_custom_view")
permissionNames should contain("can_update_custom_view")
permissionNames should contain("can_see_available_views_for_bank_account")
// Access Control permissions
permissionNames should contain("can_grant_access_to_views")
permissionNames should contain("can_revoke_access_to_views")
permissionNames should contain("can_grant_access_to_custom_views")
permissionNames should contain("can_revoke_access_to_custom_views")
}
}
}

View File

@ -16,7 +16,7 @@
<avro.version>1.8.2</avro.version> <avro.version>1.8.2</avro.version>
<lift.version>3.5.0</lift.version> <lift.version>3.5.0</lift.version>
<http4s.version>0.23.30</http4s.version> <http4s.version>0.23.30</http4s.version>
<jetty.version>9.4.58.v20250814</jetty.version> <jetty.version>9.4.50.v20250814</jetty.version>
<obp-ri.version>2016.11-RC6-SNAPSHOT</obp-ri.version> <obp-ri.version>2016.11-RC6-SNAPSHOT</obp-ri.version>
<!-- Common plugin settings --> <!-- Common plugin settings -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -9,6 +9,27 @@ Date Commit Action
- http4s.port=8086 - http4s.port=8086
These properties control the bind address of bootstrap.http4s.Http4sServer These properties control the bind address of bootstrap.http4s.Http4sServer
when running via the obp-http4s-runner fat JAR. 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 TBD TBD Performance Improvement: Added caching to getProviders endpoint
Added configurable caching with memoization to GET /obp/v6.0.0/providers endpoint. Added configurable caching with memoization to GET /obp/v6.0.0/providers endpoint.
- Default cache TTL: 3600 seconds (1 hour) - Default cache TTL: 3600 seconds (1 hour)