mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 07:56:48 +00:00
Merge remote-tracking branch 'Marko/pekko' into develop
# Conflicts: # pom.xml # release_notes.md
This commit is contained in:
commit
705de3070f
477
ideas/HTML_PAGES_REFERENCE.md
Normal file
477
ideas/HTML_PAGES_REFERENCE.md
Normal 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)
|
||||
@ -30,6 +30,7 @@ import code.CustomerDependants.MappedCustomerDependant
|
||||
import code.DynamicData.DynamicData
|
||||
import code.DynamicEndpoint.DynamicEndpoint
|
||||
import code.UserRefreshes.MappedUserRefreshes
|
||||
import code.abacrule.MappedAbacRule
|
||||
import code.accountapplication.MappedAccountApplication
|
||||
import code.accountattribute.MappedAccountAttribute
|
||||
import code.accountholders.MapperAccountHolders
|
||||
@ -1040,6 +1041,7 @@ object ToSchemify {
|
||||
MappedRegulatedEntity,
|
||||
AtmAttribute,
|
||||
Admin,
|
||||
MappedAbacRule,
|
||||
MappedBank,
|
||||
MappedBankAccount,
|
||||
BankAccountRouting,
|
||||
|
||||
150
obp-api/src/main/scala/code/abacrule/AbacRule.scala
Normal file
150
obp-api/src/main/scala/code/abacrule/AbacRule.scala
Normal 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
|
||||
}
|
||||
}
|
||||
229
obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala
Normal file
229
obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
369
obp-api/src/main/scala/code/abacrule/AbacRuleExamples.scala
Normal file
369
obp-api/src/main/scala/code/abacrule/AbacRuleExamples.scala
Normal 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
|
||||
}
|
||||
437
obp-api/src/main/scala/code/abacrule/README.md
Normal file
437
obp-api/src/main/scala/code/abacrule/README.md
Normal 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
|
||||
@ -678,6 +678,21 @@ object ApiRole extends MdcLoggable{
|
||||
case class CanGetViewPermissionsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
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
|
||||
lazy val canGetSystemLevelDynamicEntities = CanGetSystemLevelDynamicEntities()
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ object ApiTag {
|
||||
val apiTagSystemView = ResourceDocTag("View-System")
|
||||
val apiTagEntitlement = ResourceDocTag("Entitlement")
|
||||
val apiTagRole = ResourceDocTag("Role")
|
||||
val apiTagABAC = ResourceDocTag("ABAC")
|
||||
val apiTagScope = ResourceDocTag("Scope")
|
||||
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
|
||||
val apiTagCounterparty = ResourceDocTag("Counterparty")
|
||||
|
||||
@ -26,8 +26,10 @@ import code.api.v5_0_0.JSONFactory500
|
||||
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
|
||||
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
|
||||
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.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
|
||||
import code.metrics.APIMetrics
|
||||
import code.bankconnectors.LocalMappedConnectorInternal
|
||||
import code.bankconnectors.LocalMappedConnectorInternal._
|
||||
@ -74,12 +76,12 @@ trait APIMethods600 {
|
||||
|
||||
val Implementations6_0_0 = new Implementations600()
|
||||
|
||||
class Implementations600 extends MdcLoggable {
|
||||
class Implementations600 extends RestHelper with MdcLoggable {
|
||||
|
||||
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0
|
||||
|
||||
private val staticResourceDocs = ArrayBuffer[ResourceDoc]()
|
||||
def resourceDocs = staticResourceDocs
|
||||
val staticResourceDocs = ArrayBuffer[ResourceDoc]()
|
||||
val resourceDocs = staticResourceDocs
|
||||
|
||||
val apiRelations = ArrayBuffer[ApiRelation]()
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -290,6 +290,47 @@ case class CustomerWithAttributesJsonV600(
|
||||
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{
|
||||
|
||||
def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = {
|
||||
@ -735,4 +776,20 @@ case class UpdateViewJsonV600(
|
||||
def createViewsJsonV600(views: List[View]): ViewsJsonV600 = {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
143
obp-api/src/test/scala/code/api/v6_0_0/ViewPermissionsTest.scala
Normal file
143
obp-api/src/test/scala/code/api/v6_0_0/ViewPermissionsTest.scala
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
2
pom.xml
2
pom.xml
@ -16,7 +16,7 @@
|
||||
<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.58.v20250814</jetty.version>
|
||||
<jetty.version>9.4.50.v20250814</jetty.version>
|
||||
<obp-ri.version>2016.11-RC6-SNAPSHOT</obp-ri.version>
|
||||
<!-- Common plugin settings -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@ -9,6 +9,27 @@ Date Commit Action
|
||||
- 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