mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 09:26:53 +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.DynamicData.DynamicData
|
||||||
import code.DynamicEndpoint.DynamicEndpoint
|
import code.DynamicEndpoint.DynamicEndpoint
|
||||||
import code.UserRefreshes.MappedUserRefreshes
|
import code.UserRefreshes.MappedUserRefreshes
|
||||||
|
import code.abacrule.MappedAbacRule
|
||||||
import code.accountapplication.MappedAccountApplication
|
import code.accountapplication.MappedAccountApplication
|
||||||
import code.accountattribute.MappedAccountAttribute
|
import code.accountattribute.MappedAccountAttribute
|
||||||
import code.accountholders.MapperAccountHolders
|
import code.accountholders.MapperAccountHolders
|
||||||
@ -1040,6 +1041,7 @@ object ToSchemify {
|
|||||||
MappedRegulatedEntity,
|
MappedRegulatedEntity,
|
||||||
AtmAttribute,
|
AtmAttribute,
|
||||||
Admin,
|
Admin,
|
||||||
|
MappedAbacRule,
|
||||||
MappedBank,
|
MappedBank,
|
||||||
MappedBankAccount,
|
MappedBankAccount,
|
||||||
BankAccountRouting,
|
BankAccountRouting,
|
||||||
|
|||||||
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
|
case class CanGetViewPermissionsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||||
lazy val canGetViewPermissionsAtAllBanks = CanGetViewPermissionsAtAllBanks()
|
lazy val canGetViewPermissionsAtAllBanks = CanGetViewPermissionsAtAllBanks()
|
||||||
|
|
||||||
|
case class CanCreateAbacRule(requiresBankId: Boolean = false) extends ApiRole
|
||||||
|
lazy val canCreateAbacRule = CanCreateAbacRule()
|
||||||
|
|
||||||
|
case class CanGetAbacRule(requiresBankId: Boolean = false) extends ApiRole
|
||||||
|
lazy val canGetAbacRule = CanGetAbacRule()
|
||||||
|
|
||||||
|
case class CanUpdateAbacRule(requiresBankId: Boolean = false) extends ApiRole
|
||||||
|
lazy val canUpdateAbacRule = CanUpdateAbacRule()
|
||||||
|
|
||||||
|
case class CanDeleteAbacRule(requiresBankId: Boolean = false) extends ApiRole
|
||||||
|
lazy val canDeleteAbacRule = CanDeleteAbacRule()
|
||||||
|
|
||||||
|
case class CanExecuteAbacRule(requiresBankId: Boolean = false) extends ApiRole
|
||||||
|
lazy val canExecuteAbacRule = CanExecuteAbacRule()
|
||||||
|
|
||||||
case class CanGetSystemLevelDynamicEntities(requiresBankId: Boolean = false) extends ApiRole
|
case class CanGetSystemLevelDynamicEntities(requiresBankId: Boolean = false) extends ApiRole
|
||||||
lazy val canGetSystemLevelDynamicEntities = CanGetSystemLevelDynamicEntities()
|
lazy val canGetSystemLevelDynamicEntities = CanGetSystemLevelDynamicEntities()
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ object ApiTag {
|
|||||||
val apiTagSystemView = ResourceDocTag("View-System")
|
val apiTagSystemView = ResourceDocTag("View-System")
|
||||||
val apiTagEntitlement = ResourceDocTag("Entitlement")
|
val apiTagEntitlement = ResourceDocTag("Entitlement")
|
||||||
val apiTagRole = ResourceDocTag("Role")
|
val apiTagRole = ResourceDocTag("Role")
|
||||||
|
val apiTagABAC = ResourceDocTag("ABAC")
|
||||||
val apiTagScope = ResourceDocTag("Scope")
|
val apiTagScope = ResourceDocTag("Scope")
|
||||||
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
|
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
|
||||||
val apiTagCounterparty = ResourceDocTag("Counterparty")
|
val apiTagCounterparty = ResourceDocTag("Counterparty")
|
||||||
|
|||||||
@ -26,8 +26,10 @@ import code.api.v5_0_0.JSONFactory500
|
|||||||
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
|
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
|
||||||
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
|
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
|
||||||
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
|
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
|
||||||
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
|
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
|
||||||
|
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CreateAbacRuleJsonV600, ExecuteAbacRuleJsonV600, UpdateAbacRuleJsonV600}
|
||||||
import code.api.v6_0_0.OBPAPI6_0_0
|
import code.api.v6_0_0.OBPAPI6_0_0
|
||||||
|
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
|
||||||
import code.metrics.APIMetrics
|
import code.metrics.APIMetrics
|
||||||
import code.bankconnectors.LocalMappedConnectorInternal
|
import code.bankconnectors.LocalMappedConnectorInternal
|
||||||
import code.bankconnectors.LocalMappedConnectorInternal._
|
import code.bankconnectors.LocalMappedConnectorInternal._
|
||||||
@ -74,12 +76,12 @@ trait APIMethods600 {
|
|||||||
|
|
||||||
val Implementations6_0_0 = new Implementations600()
|
val Implementations6_0_0 = new Implementations600()
|
||||||
|
|
||||||
class Implementations600 extends MdcLoggable {
|
class Implementations600 extends RestHelper with MdcLoggable {
|
||||||
|
|
||||||
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0
|
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v6_0_0
|
||||||
|
|
||||||
private val staticResourceDocs = ArrayBuffer[ResourceDoc]()
|
val staticResourceDocs = ArrayBuffer[ResourceDoc]()
|
||||||
def resourceDocs = staticResourceDocs
|
val resourceDocs = staticResourceDocs
|
||||||
|
|
||||||
val apiRelations = ArrayBuffer[ApiRelation]()
|
val apiRelations = ArrayBuffer[ApiRelation]()
|
||||||
val codeContext = CodeContext(staticResourceDocs, apiRelations)
|
val codeContext = CodeContext(staticResourceDocs, apiRelations)
|
||||||
@ -4138,6 +4140,437 @@ trait APIMethods600 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ABAC Rule Endpoints
|
||||||
|
staticResourceDocs += ResourceDoc(
|
||||||
|
createAbacRule,
|
||||||
|
implementedInApiVersion,
|
||||||
|
nameOf(createAbacRule),
|
||||||
|
"POST",
|
||||||
|
"/management/abac-rules",
|
||||||
|
"Create ABAC Rule",
|
||||||
|
s"""Create a new ABAC (Attribute-Based Access Control) rule.
|
||||||
|
|
|
||||||
|
|ABAC rules are Scala functions that return a Boolean value indicating whether access should be granted.
|
||||||
|
|
|
||||||
|
|The rule function has the following signature:
|
||||||
|
|```scala
|
||||||
|
|(user: User, bankOpt: Option[Bank], accountOpt: Option[BankAccount], transactionOpt: Option[Transaction], customerOpt: Option[Customer]) => Boolean
|
||||||
|
|```
|
||||||
|
|
|
||||||
|
|Example rule code:
|
||||||
|
|```scala
|
||||||
|
|// Allow access only if user email contains "admin"
|
||||||
|
|user.emailAddress.contains("admin")
|
||||||
|
|```
|
||||||
|
|
|
||||||
|
|```scala
|
||||||
|
|// Allow access only to accounts with balance > 1000
|
||||||
|
|accountOpt.exists(_.balance.toString.toDouble > 1000.0)
|
||||||
|
|```
|
||||||
|
|
|
||||||
|
|${userAuthenticationMessage(true)}
|
||||||
|
|
|
||||||
|
|""".stripMargin,
|
||||||
|
CreateAbacRuleJsonV600(
|
||||||
|
rule_name = "admin_only",
|
||||||
|
rule_code = """user.emailAddress.contains("admin")""",
|
||||||
|
description = "Only allow access to users with admin email",
|
||||||
|
is_active = true
|
||||||
|
),
|
||||||
|
AbacRuleJsonV600(
|
||||||
|
abac_rule_id = "abc123",
|
||||||
|
rule_name = "admin_only",
|
||||||
|
rule_code = """user.emailAddress.contains("admin")""",
|
||||||
|
is_active = true,
|
||||||
|
description = "Only allow access to users with admin email",
|
||||||
|
created_by_user_id = "user123",
|
||||||
|
updated_by_user_id = "user123"
|
||||||
|
),
|
||||||
|
List(
|
||||||
|
UserNotLoggedIn,
|
||||||
|
UserHasMissingRoles,
|
||||||
|
InvalidJsonFormat,
|
||||||
|
UnknownError
|
||||||
|
),
|
||||||
|
List(apiTagABAC),
|
||||||
|
Some(List(canCreateAbacRule))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val createAbacRule: OBPEndpoint = {
|
||||||
|
case "management" :: "abac-rules" :: Nil JsonPost json -> _ => {
|
||||||
|
cc => implicit val ec = EndpointContext(Some(cc))
|
||||||
|
for {
|
||||||
|
(Full(user), callContext) <- authenticatedAccess(cc)
|
||||||
|
_ <- NewStyle.function.hasEntitlement("", user.userId, canCreateAbacRule, callContext)
|
||||||
|
createJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
|
||||||
|
json.extract[CreateAbacRuleJsonV600]
|
||||||
|
}
|
||||||
|
_ <- NewStyle.function.tryons(s"Rule name must not be empty", 400, callContext) {
|
||||||
|
createJson.rule_name.nonEmpty
|
||||||
|
}
|
||||||
|
_ <- NewStyle.function.tryons(s"Rule code must not be empty", 400, callContext) {
|
||||||
|
createJson.rule_code.nonEmpty
|
||||||
|
}
|
||||||
|
// Validate rule code by attempting to compile it
|
||||||
|
_ <- Future {
|
||||||
|
AbacRuleEngine.validateRuleCode(createJson.rule_code)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"Invalid ABAC rule code", 400)
|
||||||
|
}
|
||||||
|
rule <- Future {
|
||||||
|
MappedAbacRuleProvider.createAbacRule(
|
||||||
|
ruleName = createJson.rule_name,
|
||||||
|
ruleCode = createJson.rule_code,
|
||||||
|
description = createJson.description,
|
||||||
|
isActive = createJson.is_active,
|
||||||
|
createdBy = user.userId
|
||||||
|
)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"Could not create ABAC rule", 400)
|
||||||
|
}
|
||||||
|
} yield {
|
||||||
|
(createAbacRuleJsonV600(rule), HttpCode.`201`(callContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResourceDocs += ResourceDoc(
|
||||||
|
getAbacRule,
|
||||||
|
implementedInApiVersion,
|
||||||
|
nameOf(getAbacRule),
|
||||||
|
"GET",
|
||||||
|
"/management/abac-rules/ABAC_RULE_ID",
|
||||||
|
"Get ABAC Rule",
|
||||||
|
s"""Get an ABAC rule by its ID.
|
||||||
|
|
|
||||||
|
|${userAuthenticationMessage(true)}
|
||||||
|
|
|
||||||
|
|""".stripMargin,
|
||||||
|
EmptyBody,
|
||||||
|
AbacRuleJsonV600(
|
||||||
|
abac_rule_id = "abc123",
|
||||||
|
rule_name = "admin_only",
|
||||||
|
rule_code = """user.emailAddress.contains("admin")""",
|
||||||
|
is_active = true,
|
||||||
|
description = "Only allow access to users with admin email",
|
||||||
|
created_by_user_id = "user123",
|
||||||
|
updated_by_user_id = "user123"
|
||||||
|
),
|
||||||
|
List(
|
||||||
|
UserNotLoggedIn,
|
||||||
|
UserHasMissingRoles,
|
||||||
|
UnknownError
|
||||||
|
),
|
||||||
|
List(apiTagABAC),
|
||||||
|
Some(List(canGetAbacRule))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val getAbacRule: OBPEndpoint = {
|
||||||
|
case "management" :: "abac-rules" :: ruleId :: Nil JsonGet _ => {
|
||||||
|
cc => implicit val ec = EndpointContext(Some(cc))
|
||||||
|
for {
|
||||||
|
(Full(user), callContext) <- authenticatedAccess(cc)
|
||||||
|
_ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext)
|
||||||
|
rule <- Future {
|
||||||
|
MappedAbacRuleProvider.getAbacRuleById(ruleId)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"ABAC Rule not found with ID: $ruleId", 404)
|
||||||
|
}
|
||||||
|
} yield {
|
||||||
|
(createAbacRuleJsonV600(rule), HttpCode.`200`(callContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResourceDocs += ResourceDoc(
|
||||||
|
getAbacRules,
|
||||||
|
implementedInApiVersion,
|
||||||
|
nameOf(getAbacRules),
|
||||||
|
"GET",
|
||||||
|
"/management/abac-rules",
|
||||||
|
"Get ABAC Rules",
|
||||||
|
s"""Get all ABAC rules.
|
||||||
|
|
|
||||||
|
|${userAuthenticationMessage(true)}
|
||||||
|
|
|
||||||
|
|""".stripMargin,
|
||||||
|
EmptyBody,
|
||||||
|
AbacRulesJsonV600(
|
||||||
|
abac_rules = List(
|
||||||
|
AbacRuleJsonV600(
|
||||||
|
abac_rule_id = "abc123",
|
||||||
|
rule_name = "admin_only",
|
||||||
|
rule_code = """user.emailAddress.contains("admin")""",
|
||||||
|
is_active = true,
|
||||||
|
description = "Only allow access to users with admin email",
|
||||||
|
created_by_user_id = "user123",
|
||||||
|
updated_by_user_id = "user123"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
List(
|
||||||
|
UserNotLoggedIn,
|
||||||
|
UserHasMissingRoles,
|
||||||
|
UnknownError
|
||||||
|
),
|
||||||
|
List(apiTagABAC),
|
||||||
|
Some(List(canGetAbacRule))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val getAbacRules: OBPEndpoint = {
|
||||||
|
case "management" :: "abac-rules" :: Nil JsonGet _ => {
|
||||||
|
cc => implicit val ec = EndpointContext(Some(cc))
|
||||||
|
for {
|
||||||
|
(Full(user), callContext) <- authenticatedAccess(cc)
|
||||||
|
_ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext)
|
||||||
|
rules <- Future {
|
||||||
|
MappedAbacRuleProvider.getAllAbacRules()
|
||||||
|
}
|
||||||
|
} yield {
|
||||||
|
(createAbacRulesJsonV600(rules), HttpCode.`200`(callContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResourceDocs += ResourceDoc(
|
||||||
|
updateAbacRule,
|
||||||
|
implementedInApiVersion,
|
||||||
|
nameOf(updateAbacRule),
|
||||||
|
"PUT",
|
||||||
|
"/management/abac-rules/ABAC_RULE_ID",
|
||||||
|
"Update ABAC Rule",
|
||||||
|
s"""Update an existing ABAC rule.
|
||||||
|
|
|
||||||
|
|${userAuthenticationMessage(true)}
|
||||||
|
|
|
||||||
|
|""".stripMargin,
|
||||||
|
UpdateAbacRuleJsonV600(
|
||||||
|
rule_name = "admin_only_updated",
|
||||||
|
rule_code = """user.emailAddress.contains("admin") && user.provider == "obp"""",
|
||||||
|
description = "Only allow access to OBP admin users",
|
||||||
|
is_active = true
|
||||||
|
),
|
||||||
|
AbacRuleJsonV600(
|
||||||
|
abac_rule_id = "abc123",
|
||||||
|
rule_name = "admin_only_updated",
|
||||||
|
rule_code = """user.emailAddress.contains("admin") && user.provider == "obp"""",
|
||||||
|
is_active = true,
|
||||||
|
description = "Only allow access to OBP admin users",
|
||||||
|
created_by_user_id = "user123",
|
||||||
|
updated_by_user_id = "user456"
|
||||||
|
),
|
||||||
|
List(
|
||||||
|
UserNotLoggedIn,
|
||||||
|
UserHasMissingRoles,
|
||||||
|
InvalidJsonFormat,
|
||||||
|
UnknownError
|
||||||
|
),
|
||||||
|
List(apiTagABAC),
|
||||||
|
Some(List(canUpdateAbacRule))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val updateAbacRule: OBPEndpoint = {
|
||||||
|
case "management" :: "abac-rules" :: ruleId :: Nil JsonPut json -> _ => {
|
||||||
|
cc => implicit val ec = EndpointContext(Some(cc))
|
||||||
|
for {
|
||||||
|
(Full(user), callContext) <- authenticatedAccess(cc)
|
||||||
|
_ <- NewStyle.function.hasEntitlement("", user.userId, canUpdateAbacRule, callContext)
|
||||||
|
updateJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
|
||||||
|
json.extract[UpdateAbacRuleJsonV600]
|
||||||
|
}
|
||||||
|
// Validate rule code by attempting to compile it
|
||||||
|
_ <- Future {
|
||||||
|
AbacRuleEngine.validateRuleCode(updateJson.rule_code)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"Invalid ABAC rule code", 400)
|
||||||
|
}
|
||||||
|
rule <- Future {
|
||||||
|
MappedAbacRuleProvider.updateAbacRule(
|
||||||
|
ruleId = ruleId,
|
||||||
|
ruleName = updateJson.rule_name,
|
||||||
|
ruleCode = updateJson.rule_code,
|
||||||
|
description = updateJson.description,
|
||||||
|
isActive = updateJson.is_active,
|
||||||
|
updatedBy = user.userId
|
||||||
|
)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"Could not update ABAC rule with ID: $ruleId", 400)
|
||||||
|
}
|
||||||
|
// Clear rule from cache after update
|
||||||
|
_ <- Future {
|
||||||
|
AbacRuleEngine.clearRuleFromCache(ruleId)
|
||||||
|
}
|
||||||
|
} yield {
|
||||||
|
(createAbacRuleJsonV600(rule), HttpCode.`200`(callContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResourceDocs += ResourceDoc(
|
||||||
|
deleteAbacRule,
|
||||||
|
implementedInApiVersion,
|
||||||
|
nameOf(deleteAbacRule),
|
||||||
|
"DELETE",
|
||||||
|
"/management/abac-rules/ABAC_RULE_ID",
|
||||||
|
"Delete ABAC Rule",
|
||||||
|
s"""Delete an ABAC rule.
|
||||||
|
|
|
||||||
|
|${userAuthenticationMessage(true)}
|
||||||
|
|
|
||||||
|
|""".stripMargin,
|
||||||
|
EmptyBody,
|
||||||
|
EmptyBody,
|
||||||
|
List(
|
||||||
|
UserNotLoggedIn,
|
||||||
|
UserHasMissingRoles,
|
||||||
|
UnknownError
|
||||||
|
),
|
||||||
|
List(apiTagABAC),
|
||||||
|
Some(List(canDeleteAbacRule))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val deleteAbacRule: OBPEndpoint = {
|
||||||
|
case "management" :: "abac-rules" :: ruleId :: Nil JsonDelete _ => {
|
||||||
|
cc => implicit val ec = EndpointContext(Some(cc))
|
||||||
|
for {
|
||||||
|
(Full(user), callContext) <- authenticatedAccess(cc)
|
||||||
|
_ <- NewStyle.function.hasEntitlement("", user.userId, canDeleteAbacRule, callContext)
|
||||||
|
deleted <- Future {
|
||||||
|
MappedAbacRuleProvider.deleteAbacRule(ruleId)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"Could not delete ABAC rule with ID: $ruleId", 400)
|
||||||
|
}
|
||||||
|
// Clear rule from cache after deletion
|
||||||
|
_ <- Future {
|
||||||
|
AbacRuleEngine.clearRuleFromCache(ruleId)
|
||||||
|
}
|
||||||
|
} yield {
|
||||||
|
(Full(deleted), HttpCode.`204`(callContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
staticResourceDocs += ResourceDoc(
|
||||||
|
executeAbacRule,
|
||||||
|
implementedInApiVersion,
|
||||||
|
nameOf(executeAbacRule),
|
||||||
|
"POST",
|
||||||
|
"/management/abac-rules/ABAC_RULE_ID/execute",
|
||||||
|
"Execute ABAC Rule",
|
||||||
|
s"""Execute an ABAC rule to test access control.
|
||||||
|
|
|
||||||
|
|This endpoint allows you to test an ABAC rule with specific context (bank, account, transaction, customer).
|
||||||
|
|
|
||||||
|
|${userAuthenticationMessage(true)}
|
||||||
|
|
|
||||||
|
|""".stripMargin,
|
||||||
|
ExecuteAbacRuleJsonV600(
|
||||||
|
bank_id = Some("gh.29.uk"),
|
||||||
|
account_id = Some("8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"),
|
||||||
|
transaction_id = None,
|
||||||
|
customer_id = None
|
||||||
|
),
|
||||||
|
AbacRuleResultJsonV600(
|
||||||
|
rule_id = "abc123",
|
||||||
|
rule_name = "admin_only",
|
||||||
|
result = true,
|
||||||
|
message = "Access granted"
|
||||||
|
),
|
||||||
|
List(
|
||||||
|
UserNotLoggedIn,
|
||||||
|
UserHasMissingRoles,
|
||||||
|
InvalidJsonFormat,
|
||||||
|
UnknownError
|
||||||
|
),
|
||||||
|
List(apiTagABAC),
|
||||||
|
Some(List(canExecuteAbacRule))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val executeAbacRule: OBPEndpoint = {
|
||||||
|
case "management" :: "abac-rules" :: ruleId :: "execute" :: Nil JsonPost json -> _ => {
|
||||||
|
cc => implicit val ec = EndpointContext(Some(cc))
|
||||||
|
for {
|
||||||
|
(Full(user), callContext) <- authenticatedAccess(cc)
|
||||||
|
_ <- NewStyle.function.hasEntitlement("", user.userId, canExecuteAbacRule, callContext)
|
||||||
|
execJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
|
||||||
|
json.extract[ExecuteAbacRuleJsonV600]
|
||||||
|
}
|
||||||
|
rule <- Future {
|
||||||
|
MappedAbacRuleProvider.getAbacRuleById(ruleId)
|
||||||
|
} map {
|
||||||
|
unboxFullOrFail(_, callContext, s"ABAC Rule not found with ID: $ruleId", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch context objects if IDs are provided
|
||||||
|
bankOpt <- execJson.bank_id match {
|
||||||
|
case Some(bankId) => NewStyle.function.getBank(BankId(bankId), callContext).map { case (bank, _) => Some(bank) }
|
||||||
|
case None => Future.successful(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountOpt <- execJson.account_id match {
|
||||||
|
case Some(accountId) if execJson.bank_id.isDefined =>
|
||||||
|
NewStyle.function.getBankAccount(BankId(execJson.bank_id.get), AccountId(accountId), callContext)
|
||||||
|
.map { case (account, _) => Some(account) }
|
||||||
|
case _ => Future.successful(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionOpt <- execJson.transaction_id match {
|
||||||
|
case Some(transId) if execJson.bank_id.isDefined && execJson.account_id.isDefined =>
|
||||||
|
NewStyle.function.getTransaction(
|
||||||
|
BankId(execJson.bank_id.get),
|
||||||
|
AccountId(execJson.account_id.get),
|
||||||
|
TransactionId(transId),
|
||||||
|
callContext
|
||||||
|
).map { case (transaction, _) => Some(transaction) }.recover { case _ => None }
|
||||||
|
case _ => Future.successful(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
customerOpt <- execJson.customer_id match {
|
||||||
|
case Some(custId) if execJson.bank_id.isDefined =>
|
||||||
|
NewStyle.function.getCustomerByCustomerId(custId, callContext)
|
||||||
|
.map { case (customer, _) => Some(customer) }.recover { case _ => None }
|
||||||
|
case _ => Future.successful(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the rule
|
||||||
|
result <- Future {
|
||||||
|
AbacRuleEngine.executeRule(
|
||||||
|
ruleId = ruleId,
|
||||||
|
user = user,
|
||||||
|
bankOpt = bankOpt,
|
||||||
|
accountOpt = accountOpt,
|
||||||
|
transactionOpt = transactionOpt,
|
||||||
|
customerOpt = customerOpt
|
||||||
|
)
|
||||||
|
} map {
|
||||||
|
case Full(allowed) =>
|
||||||
|
AbacRuleResultJsonV600(
|
||||||
|
rule_id = ruleId,
|
||||||
|
rule_name = rule.ruleName,
|
||||||
|
result = allowed,
|
||||||
|
message = if (allowed) "Access granted" else "Access denied"
|
||||||
|
)
|
||||||
|
case Failure(msg, _, _) =>
|
||||||
|
AbacRuleResultJsonV600(
|
||||||
|
rule_id = ruleId,
|
||||||
|
rule_name = rule.ruleName,
|
||||||
|
result = false,
|
||||||
|
message = s"Execution error: $msg"
|
||||||
|
)
|
||||||
|
case Empty =>
|
||||||
|
AbacRuleResultJsonV600(
|
||||||
|
rule_id = ruleId,
|
||||||
|
rule_name = rule.ruleName,
|
||||||
|
result = false,
|
||||||
|
message = "Execution failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} yield {
|
||||||
|
(result, HttpCode.`200`(callContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -290,6 +290,47 @@ case class CustomerWithAttributesJsonV600(
|
|||||||
customer_attributes: List[CustomerAttributeResponseJsonV300]
|
customer_attributes: List[CustomerAttributeResponseJsonV300]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ABAC Rule JSON models
|
||||||
|
case class CreateAbacRuleJsonV600(
|
||||||
|
rule_name: String,
|
||||||
|
rule_code: String,
|
||||||
|
description: String,
|
||||||
|
is_active: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
case class UpdateAbacRuleJsonV600(
|
||||||
|
rule_name: String,
|
||||||
|
rule_code: String,
|
||||||
|
description: String,
|
||||||
|
is_active: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
case class AbacRuleJsonV600(
|
||||||
|
abac_rule_id: String,
|
||||||
|
rule_name: String,
|
||||||
|
rule_code: String,
|
||||||
|
is_active: Boolean,
|
||||||
|
description: String,
|
||||||
|
created_by_user_id: String,
|
||||||
|
updated_by_user_id: String
|
||||||
|
)
|
||||||
|
|
||||||
|
case class AbacRulesJsonV600(abac_rules: List[AbacRuleJsonV600])
|
||||||
|
|
||||||
|
case class ExecuteAbacRuleJsonV600(
|
||||||
|
bank_id: Option[String],
|
||||||
|
account_id: Option[String],
|
||||||
|
transaction_id: Option[String],
|
||||||
|
customer_id: Option[String]
|
||||||
|
)
|
||||||
|
|
||||||
|
case class AbacRuleResultJsonV600(
|
||||||
|
rule_id: String,
|
||||||
|
rule_name: String,
|
||||||
|
result: Boolean,
|
||||||
|
message: String
|
||||||
|
)
|
||||||
|
|
||||||
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
|
object JSONFactory600 extends CustomJsonFormats with MdcLoggable{
|
||||||
|
|
||||||
def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = {
|
def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = {
|
||||||
@ -735,4 +776,20 @@ case class UpdateViewJsonV600(
|
|||||||
def createViewsJsonV600(views: List[View]): ViewsJsonV600 = {
|
def createViewsJsonV600(views: List[View]): ViewsJsonV600 = {
|
||||||
ViewsJsonV600(views.map(createViewJsonV600))
|
ViewsJsonV600(views.map(createViewJsonV600))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def createAbacRuleJsonV600(rule: code.abacrule.AbacRule): AbacRuleJsonV600 = {
|
||||||
|
AbacRuleJsonV600(
|
||||||
|
abac_rule_id = rule.abacRuleId,
|
||||||
|
rule_name = rule.ruleName,
|
||||||
|
rule_code = rule.ruleCode,
|
||||||
|
is_active = rule.isActive,
|
||||||
|
description = rule.description,
|
||||||
|
created_by_user_id = rule.createdByUserId,
|
||||||
|
updated_by_user_id = rule.updatedByUserId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def createAbacRulesJsonV600(rules: List[code.abacrule.AbacRule]): AbacRulesJsonV600 = {
|
||||||
|
AbacRulesJsonV600(rules.map(createAbacRuleJsonV600))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
<avro.version>1.8.2</avro.version>
|
||||||
<lift.version>3.5.0</lift.version>
|
<lift.version>3.5.0</lift.version>
|
||||||
<http4s.version>0.23.30</http4s.version>
|
<http4s.version>0.23.30</http4s.version>
|
||||||
<jetty.version>9.4.58.v20250814</jetty.version>
|
<jetty.version>9.4.50.v20250814</jetty.version>
|
||||||
<obp-ri.version>2016.11-RC6-SNAPSHOT</obp-ri.version>
|
<obp-ri.version>2016.11-RC6-SNAPSHOT</obp-ri.version>
|
||||||
<!-- Common plugin settings -->
|
<!-- Common plugin settings -->
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|||||||
@ -9,6 +9,27 @@ Date Commit Action
|
|||||||
- http4s.port=8086
|
- http4s.port=8086
|
||||||
These properties control the bind address of bootstrap.http4s.Http4sServer
|
These properties control the bind address of bootstrap.http4s.Http4sServer
|
||||||
when running via the obp-http4s-runner fat JAR.
|
when running via the obp-http4s-runner fat JAR.
|
||||||
|
11/12/2025 3c2df942 BREAKING CHANGE: Migration from Akka to Apache Pekko™ 1.1.2
|
||||||
|
Replaced Akka 2.5.32 with Apache Pekko™ 1.1.2 to address Akka licensing changes.
|
||||||
|
Updated all imports from com.typesafe.akka to org.apache.pekko.
|
||||||
|
Updated Jetty from 9.4.50 to 9.4.58 for improved Java 17 compatibility.
|
||||||
|
|
||||||
|
Migrated all actor systems to Apache Pekko™ and fixed critical scheduler
|
||||||
|
actor system initialization conflicts.
|
||||||
|
Consolidated all schedulers to use shared ObpActorSystem.localActorSystem.
|
||||||
|
Prevented multiple actor system creation during application boot.
|
||||||
|
|
||||||
|
Fixed actor system references in all schedulers:
|
||||||
|
- DataBaseCleanerScheduler
|
||||||
|
- DatabaseDriverScheduler
|
||||||
|
- MetricsArchiveScheduler
|
||||||
|
- SchedulerUtil
|
||||||
|
- TransactionRequestStatusScheduler
|
||||||
|
|
||||||
|
Resolved 'Address already in use' port binding errors.
|
||||||
|
Eliminated ExceptionInInitializerError during startup.
|
||||||
|
Fixed race conditions in actor system initialization.
|
||||||
|
All scheduler functionality preserved with improved stability.
|
||||||
TBD TBD Performance Improvement: Added caching to getProviders endpoint
|
TBD TBD Performance Improvement: Added caching to getProviders endpoint
|
||||||
Added configurable caching with memoization to GET /obp/v6.0.0/providers endpoint.
|
Added configurable caching with memoization to GET /obp/v6.0.0/providers endpoint.
|
||||||
- Default cache TTL: 3600 seconds (1 hour)
|
- Default cache TTL: 3600 seconds (1 hour)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user