From 77ecfc6c12a438886452a1fdf280a2d1ba5c1ea0 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 14:59:29 +0100 Subject: [PATCH 01/19] flushall_build_and_run.sh --- flushall_build_and_run.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100755 flushall_build_and_run.sh diff --git a/flushall_build_and_run.sh b/flushall_build_and_run.sh new file mode 100755 index 000000000..b38550f72 --- /dev/null +++ b/flushall_build_and_run.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Script to flush Redis, build the project, and run Jetty +# +# This script should be run from the OBP-API root directory: +# cd /path/to/OBP-API +# ./flushall_build_and_run.sh + +set -e # Exit on error + +echo "==========================================" +echo "Flushing Redis cache..." +echo "==========================================" +redis-cli < Date: Tue, 16 Dec 2025 18:58:30 +0100 Subject: [PATCH 02/19] docfix: ABAC glossary items in resource doc --- .../scala/code/api/v6_0_0/APIMethods600.scala | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index cea75c73b..3931ad03a 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4153,10 +4153,10 @@ trait APIMethods600 { |ABAC rules are Scala functions that return a Boolean value indicating whether access should be granted. | |**Documentation:** - |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules - |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters - |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference - |- [ABAC Testing Examples](glossary#ABAC_Testing_Examples.md) - Testing examples and patterns + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference + |- ${Glossary.getGlossaryItemLink("ABAC_Testing_Examples")} - Testing examples and patterns | |The rule function receives 18 parameters including authenticatedUser, attributes, auth context, and optional objects (bank, account, transaction, etc.). | @@ -4247,9 +4247,9 @@ trait APIMethods600 { s"""Get an ABAC rule by its ID. | |**Documentation:** - |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules - |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters - |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference | |${userAuthenticationMessage(true)} | @@ -4300,9 +4300,9 @@ trait APIMethods600 { s"""Get all ABAC rules. | |**Documentation:** - |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules - |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters - |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference | |${userAuthenticationMessage(true)} | @@ -4355,9 +4355,9 @@ trait APIMethods600 { s"""Update an existing ABAC rule. | |**Documentation:** - |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules - |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters - |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference | |${userAuthenticationMessage(true)} | @@ -4434,8 +4434,8 @@ trait APIMethods600 { s"""Delete an ABAC rule by its ID. | |**Documentation:** - |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules - |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters | |${userAuthenticationMessage(true)} | @@ -4484,10 +4484,10 @@ trait APIMethods600 { |This endpoint allows you to test an ABAC rule with specific context (authenticated user, bank, account, transaction, customer, etc.). | |**Documentation:** - |- [ABAC Simple Guide](glossary#ABAC_Simple_Guide.md) - Getting started with ABAC rules - |- [ABAC Parameters Summary](glossary#ABAC_Parameters_Summary.md) - Complete list of all 18 parameters - |- [ABAC Object Properties Reference](glossary#ABAC_Object_Properties_Reference.md) - Detailed property reference - |- [ABAC Testing Examples](glossary#ABAC_Testing_Examples.md) - Testing examples and patterns + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference + |- ${Glossary.getGlossaryItemLink("ABAC_Testing_Examples")} - Testing examples and patterns | |You can provide optional IDs in the request body to test the rule with specific context. | From 6212afedea74468491ea2b6650bd652df3298a57 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 19:55:12 +0100 Subject: [PATCH 03/19] docfix: moving ABAC glossary items to Glossary.scala --- .../ABAC_Object_Properties_Reference.md | 856 ------------------ .../docs/glossary/ABAC_Parameters_Summary.md | 267 ------ .../docs/glossary/ABAC_Simple_Guide.md | 354 -------- .../docs/glossary/ABAC_Testing_Examples.md | 622 ------------- .../main/scala/code/api/util/Glossary.scala | 462 ++++++++++ 5 files changed, 462 insertions(+), 2099 deletions(-) delete mode 100644 obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md delete mode 100644 obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md delete mode 100644 obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md delete mode 100644 obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md b/obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md deleted file mode 100644 index d91148ab8..000000000 --- a/obp-api/src/main/resources/docs/glossary/ABAC_Object_Properties_Reference.md +++ /dev/null @@ -1,856 +0,0 @@ -# ABAC Rule Object Properties Reference - -This document provides a comprehensive reference for all properties available on objects that can be used in ABAC (Attribute-Based Access Control) rules. - -## Overview - -When you write ABAC rules, you have access to eleven objects: - -1. **authenticatedUser** - The authenticated user making the API call (always available) -2. **authenticatedUserAttributes** - Non-personal attributes for the authenticated user (always available) -3. **authenticatedUserAuthContext** - Auth context for the authenticated user (always available) -4. **onBehalfOfUserOpt** - Optional user for delegation scenarios -5. **onBehalfOfUserAttributes** - Non-personal attributes for the onBehalfOf user (always available, may be empty) -6. **onBehalfOfUserAuthContext** - Auth context for the onBehalfOf user (always available, may be empty) -7. **user** - A user object (always available) -8. **bankOpt** - Optional bank context -9. **accountOpt** - Optional account context -10. **transactionOpt** - Optional transaction context -11. **customerOpt** - Optional customer context - -**Important: All objects are READ-ONLY.** You cannot modify user attributes, auth context, or any other objects within ABAC rules. - -## How to Use This Reference - -When writing ABAC rules, you can access properties using dot notation: - -```scala -// Example: Check if authenticated user is admin -authenticatedUser.emailAddress.endsWith("@admin.com") - -// Example: Check authenticated user attributes -authenticatedUserAttributes.exists(attr => attr.name == "department" && attr.value == "finance") - -// Example: Check authenticated user auth context -authenticatedUserAuthContext.exists(ctx => ctx.key == "session_id") - -// Example: Check if delegation is present -onBehalfOfUserOpt.isDefined - -// Example: Check onBehalfOf user attributes -onBehalfOfUserAttributes.exists(attr => attr.name == "role" && attr.value == "manager") - -// Example: Check onBehalfOf user auth context -onBehalfOfUserAuthContext.exists(ctx => ctx.key == "device_id") - -// Example: Check if user has specific email -user.emailAddress == "alice@example.com" - -// Example: Check if account balance is above 1000 -accountOpt.exists(account => account.balance.toDouble > 1000.0) - -// Example: Check if bank is in UK -bankOpt.exists(bank => bank.bankId.value.startsWith("gh.")) -``` - ---- - -## 1. authenticatedUser (User) - -The authenticated user making the API call. This is always available (not optional). - -### Available Properties - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `userId` | `String` | Unique UUID for the user | `"f47ac10b-58cc-4372-a567-0e02b2c3d479"` | -| `idGivenByProvider` | `String` | Same as username | `"alice@example.com"` | -| `provider` | `String` | Authentication provider | `"obp"`, `"oauth"`, `"openid"` | -| `emailAddress` | `String` | User's email address | `"alice@example.com"` | -| `name` | `String` | User's full name | `"Alice Smith"` | -| `createdByConsentId` | `Option[String]` | Consent ID if user created via consent | `Some("consent-123")` or `None` | -| `createdByUserInvitationId` | `Option[String]` | User invitation ID if applicable | `Some("invite-456")` or `None` | -| `isDeleted` | `Option[Boolean]` | Whether user is deleted | `Some(false)` or `None` | -| `lastMarketingAgreementSignedDate` | `Option[Date]` | Last marketing agreement date | `Some(Date)` or `None` | -| `lastUsedLocale` | `Option[String]` | Last used locale/language | `Some("en_GB")` or `None` | - -### Helper Methods - -| Method | Type | Description | -|--------|------|-------------| -| `isOriginalUser` | `Boolean` | True if user created by OBP (not via consent) | -| `isConsentUser` | `Boolean` | True if user created via consent | - -### Example Rules Using authenticatedUser - -```scala -// 1. Allow only admin users (by email suffix) -authenticatedUser.emailAddress.endsWith("@admin.com") - -// 2. Allow specific user by ID -authenticatedUser.userId == "f47ac10b-58cc-4372-a567-0e02b2c3d479" - -// 3. Allow only original users (not consent users) -authenticatedUser.isOriginalUser - -// 4. Check if user has name -authenticatedUser.name.nonEmpty - -// 5. Check authentication provider -authenticatedUser.provider == "obp" - -// 6. Complex condition -authenticatedUser.emailAddress.endsWith("@admin.com") || -authenticatedUser.name.contains("Manager") -``` - ---- - -## 2. authenticatedUserAttributes (List[UserAttribute]) - -Non-personal attributes for the authenticated user. This is always available (not optional), but may be an empty list. - -### UserAttribute Properties - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `userAttributeId` | `String` | Unique attribute ID | `"attr-123"` | -| `userId` | `String` | User ID this attribute belongs to | `"user-456"` | -| `name` | `String` | Attribute name | `"department"`, `"role"`, `"clearance_level"` | -| `attributeType` | `UserAttributeType.Value` | Type of attribute | `UserAttributeType.STRING`, `UserAttributeType.INTEGER` | -| `value` | `String` | Attribute value | `"finance"`, `"manager"`, `"5"` | -| `insertDate` | `Date` | When attribute was created | `Date(...)` | -| `isPersonal` | `Boolean` | Whether attribute is personal (always false here) | `false` | - -### Example Rules Using authenticatedUserAttributes - -```scala -// 1. Check if user has a specific attribute -authenticatedUserAttributes.exists(attr => - attr.name == "department" && attr.value == "finance" -) - -// 2. Check if user has clearance level >= 3 -authenticatedUserAttributes.exists(attr => - attr.name == "clearance_level" && - attr.value.toIntOption.exists(_ >= 3) -) - -// 3. Check if user has any attributes -authenticatedUserAttributes.nonEmpty - -// 4. Check multiple attributes (AND) -val hasDepartment = authenticatedUserAttributes.exists(_.name == "department") -val hasRole = authenticatedUserAttributes.exists(_.name == "role") -hasDepartment && hasRole - -// 5. Get specific attribute value -val departmentOpt = authenticatedUserAttributes.find(_.name == "department").map(_.value) -departmentOpt.contains("finance") - -// 6. Check attribute with multiple possible values (OR) -authenticatedUserAttributes.exists(attr => - attr.name == "role" && - List("admin", "manager", "supervisor").contains(attr.value) -) - -// 7. Combine with user properties -authenticatedUser.emailAddress.endsWith("@admin.com") || -authenticatedUserAttributes.exists(attr => attr.name == "admin_override" && attr.value == "true") -``` - ---- - -## 3. authenticatedUserAuthContext (List[UserAuthContext]) - -Authentication context for the authenticated user. This is always available (not optional), but may be an empty list. - -**READ-ONLY:** These values cannot be modified within ABAC rules. - -### UserAuthContext Properties - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `userAuthContextId` | `String` | Unique auth context ID | `"ctx-123"` | -| `userId` | `String` | User ID this context belongs to | `"user-456"` | -| `key` | `String` | Context key | `"session_id"`, `"ip_address"`, `"device_id"` | -| `value` | `String` | Context value | `"sess-abc-123"`, `"192.168.1.1"`, `"device-xyz"` | -| `timeStamp` | `Date` | When context was created | `Date(...)` | -| `consumerId` | `String` | Consumer/app that created this context | `"consumer-789"` | - -### Example Rules Using authenticatedUserAuthContext - -```scala -// 1. Check if user has a specific auth context -authenticatedUserAuthContext.exists(ctx => - ctx.key == "ip_address" && ctx.value.startsWith("192.168.") -) - -// 2. Check if session exists -authenticatedUserAuthContext.exists(ctx => ctx.key == "session_id") - -// 3. Check if auth context was recently created (within last hour) -import java.time.Instant -import java.time.temporal.ChronoUnit - -authenticatedUserAuthContext.exists(ctx => { - val now = Instant.now() - val ctxInstant = ctx.timeStamp.toInstant - ChronoUnit.HOURS.between(ctxInstant, now) < 1 -}) - -// 4. Check multiple context values (AND) -val hasSession = authenticatedUserAuthContext.exists(_.key == "session_id") -val hasDevice = authenticatedUserAuthContext.exists(_.key == "device_id") -hasSession && hasDevice - -// 5. Get specific context value -val ipAddressOpt = authenticatedUserAuthContext.find(_.key == "ip_address").map(_.value) -ipAddressOpt.exists(ip => ip.startsWith("10.0.")) - -// 6. Check consumer ID -authenticatedUserAuthContext.exists(ctx => - ctx.consumerId == "trusted-consumer-123" -) - -// 7. Combine with user properties -authenticatedUser.emailAddress.endsWith("@admin.com") && -authenticatedUserAuthContext.exists(_.key == "mfa_verified" && _.value == "true") -``` - ---- - -## 4. onBehalfOfUserOpt (Option[User]) - -Optional user for delegation scenarios. Present when someone acts on behalf of another user. - -This is an `Option[User]` - use `.exists()`, `.isDefined`, `.isEmpty`, or pattern matching. - -### Available Properties (when present) - -Same properties as `authenticatedUser` (see section 1 above). - -**Note:** When `onBehalfOfUserOpt` is present, the corresponding `onBehalfOfUserAttributes` and `onBehalfOfUserAuthContext` lists will contain data for that user. - -### Example Rules Using onBehalfOfUserOpt - -```scala -// 1. Check if delegation is being used -onBehalfOfUserOpt.isDefined - -// 2. Check if no delegation (direct access only) -onBehalfOfUserOpt.isEmpty - -// 3. Check delegation user's email -onBehalfOfUserOpt.exists(delegatedUser => - delegatedUser.emailAddress.endsWith("@company.com") -) - -// 4. Allow if authenticated user is customer service AND delegation is used -val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com") -val hasDelegation = onBehalfOfUserOpt.isDefined -isCustomerService && hasDelegation - -// 5. Check both authenticated and delegation users -onBehalfOfUserOpt match { - case Some(delegatedUser) => - authenticatedUser.emailAddress.endsWith("@admin.com") && - delegatedUser.emailAddress.nonEmpty - case None => true // No delegation, allow -} -``` - ---- - -## 5. onBehalfOfUserAttributes (List[UserAttribute]) - -Non-personal attributes for the onBehalfOf user. This is always available (not optional), but will be an empty list if no delegation is happening. - -**READ-ONLY:** These values cannot be modified within ABAC rules. - -### UserAttribute Properties - -Same properties as `authenticatedUserAttributes` (see section 2 above). - -### Example Rules Using onBehalfOfUserAttributes - -```scala -// 1. Check if onBehalfOf user has specific attribute -onBehalfOfUserAttributes.exists(attr => - attr.name == "department" && attr.value == "sales" -) - -// 2. Check if onBehalfOf user has attributes (delegation with data) -onBehalfOfUserAttributes.nonEmpty - -// 3. Verify delegation user has required role -onBehalfOfUserOpt.isDefined && -onBehalfOfUserAttributes.exists(attr => - attr.name == "role" && attr.value == "manager" -) - -// 4. Compare authenticated and onBehalfOf user departments -val authDept = authenticatedUserAttributes.find(_.name == "department").map(_.value) -val onBehalfDept = onBehalfOfUserAttributes.find(_.name == "department").map(_.value) -authDept == onBehalfDept - -// 5. Check clearance level for delegation -onBehalfOfUserAttributes.exists(attr => - attr.name == "clearance_level" && - attr.value.toIntOption.exists(_ >= 2) -) -``` - ---- - -## 6. onBehalfOfUserAuthContext (List[UserAuthContext]) - -Authentication context for the onBehalfOf user. This is always available (not optional), but will be an empty list if no delegation is happening. - -**READ-ONLY:** These values cannot be modified within ABAC rules. - -### UserAuthContext Properties - -Same properties as `authenticatedUserAuthContext` (see section 3 above). - -### Example Rules Using onBehalfOfUserAuthContext - -```scala -// 1. Check if onBehalfOf user has active session -onBehalfOfUserAuthContext.exists(ctx => ctx.key == "session_id") - -// 2. Verify onBehalfOf user IP is from internal network -onBehalfOfUserAuthContext.exists(ctx => - ctx.key == "ip_address" && ctx.value.startsWith("10.0.") -) - -// 3. Check if both authenticated and onBehalfOf users have MFA -val authHasMFA = authenticatedUserAuthContext.exists(_.key == "mfa_verified") -val onBehalfHasMFA = onBehalfOfUserAuthContext.exists(_.key == "mfa_verified") -authHasMFA && onBehalfHasMFA - -// 4. Verify delegation has auth context -onBehalfOfUserOpt.isDefined && onBehalfOfUserAuthContext.nonEmpty - -// 5. Check consumer for delegation -onBehalfOfUserAuthContext.exists(ctx => - ctx.consumerId == "trusted-consumer-123" -) -``` - ---- - -## 7. user (User) - -A user object. This is always available (not optional). - -### Available Properties - -Same properties as `authenticatedUser` (see section 1 above). - -### Example Rules Using user - -```scala -// 1. Check user email -user.emailAddress == "alice@example.com" - -// 2. Check user by ID -user.userId == "f47ac10b-58cc-4372-a567-0e02b2c3d479" - -// 3. Check user provider -user.provider == "obp" - -// 4. Compare with authenticated user -user.userId == authenticatedUser.userId - -// 5. Check if user owns account (if ownership data available) -accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) -``` - ---- - -## 8. bankOpt (Option[Bank]) - -Optional bank context. Present when `bank_id` is provided in the API request. - -### Available Properties (when present) - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `bankId` | `BankId` | Unique bank identifier | `BankId("gh.29.uk")` | -| `shortName` | `String` | Short name of bank | `"GH Bank"` | -| `fullName` | `String` | Full legal name | `"Great Britain Bank Ltd"` | -| `logoUrl` | `String` | URL to bank logo | `"https://example.com/logo.png"` | -| `websiteUrl` | `String` | Bank website URL | `"https://www.ghbank.co.uk"` | -| `bankRoutingScheme` | `String` | Routing scheme | `"SWIFT_BIC"`, `"UK.SORTCODE"` | -| `bankRoutingAddress` | `String` | Routing address/code | `"GHBKGB2L"` | -| `swiftBic` | `String` | SWIFT BIC code (deprecated) | `"GHBKGB2L"` | -| `nationalIdentifier` | `String` | National identifier (deprecated) | `"123456"` | - -### Accessing BankId Value - -```scala -// Get the string value from BankId -bankOpt.exists(bank => bank.bankId.value == "gh.29.uk") -``` - -### Example Rules Using bankOpt - -```scala -// 1. Allow only UK banks (by ID prefix) -bankOpt.exists(bank => - bank.bankId.value.startsWith("gh.") || - bank.bankId.value.startsWith("uk.") -) - -// 2. Allow specific bank -bankOpt.exists(bank => bank.bankId.value == "gh.29.uk") - -// 3. Check bank name -bankOpt.exists(bank => bank.shortName.contains("GH")) - -// 4. Check SWIFT BIC -bankOpt.exists(bank => bank.swiftBic.startsWith("GHBK")) - -// 5. Allow if no bank context provided -bankOpt.isEmpty - -// 6. Check website URL -bankOpt.exists(bank => bank.websiteUrl.contains(".uk")) -``` - ---- - -## 9. accountOpt (Option[BankAccount]) - -Optional bank account context. Present when `account_id` is provided in the API request. - -### Available Properties (when present) - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `accountId` | `AccountId` | Unique account identifier | `AccountId("8ca8a7e4-6d02-48e3...")` | -| `accountType` | `String` | Type of account | `"CURRENT"`, `"SAVINGS"`, `"330"` | -| `balance` | `BigDecimal` | Current account balance | `1234.56` | -| `currency` | `String` | Currency code (ISO 4217) | `"GBP"`, `"EUR"`, `"USD"` | -| `name` | `String` | Account name | `"Main Checking Account"` | -| `label` | `String` | Account label | `"Personal Account"` | -| `number` | `String` | Account number | `"12345678"` | -| `bankId` | `BankId` | Bank identifier | `BankId("gh.29.uk")` | -| `lastUpdate` | `Date` | Last transaction refresh date | `Date(...)` | -| `branchId` | `String` | Branch identifier | `"branch-123"` | -| `accountRoutings` | `List[AccountRouting]` | Account routing information | `List(AccountRouting(...))` | -| `accountRules` | `List[AccountRule]` | Account rules (optional) | `List(...)` | -| `accountHolder` | `String` | Account holder name (deprecated) | `"Alice Smith"` | -| `attributes` | `Option[List[Attribute]]` | Account attributes | `Some(List(...))` or `None` | - -### Important Notes - -- `balance` is a `BigDecimal` - convert to `Double` if needed: `account.balance.toDouble` -- `accountId.value` gives the string value -- `bankId.value` gives the bank ID string -- Use `accountOpt.exists()` to safely check properties - -### Example Rules Using accountOpt - -```scala -// 1. Check minimum balance -accountOpt.exists(account => account.balance.toDouble >= 1000.0) - -// 2. Check account currency -accountOpt.exists(account => account.currency == "GBP") - -// 3. Check account type -accountOpt.exists(account => account.accountType == "CURRENT") - -// 4. Check account belongs to specific bank -accountOpt.exists(account => account.bankId.value == "gh.29.uk") - -// 5. Check account number -accountOpt.exists(account => account.number.startsWith("123")) - -// 6. Check if account has label -accountOpt.exists(account => account.label.nonEmpty) - -// 7. Complex balance and currency check -accountOpt.exists(account => - account.balance.toDouble > 5000.0 && - account.currency == "GBP" -) - -// 8. Check account attributes (if available) -accountOpt.exists(account => - account.attributes.exists(attrs => - attrs.exists(attr => attr.name == "accountStatus" && attr.value == "active") - ) -) -``` - ---- - -## 10. transactionOpt (Option[Transaction]) - -Optional transaction context. Present when `transaction_id` is provided in the API request. - -Uses the `TransactionCore` type. - -### Available Properties (when present) - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `id` | `TransactionId` | Unique transaction identifier | `TransactionId("trans-123")` | -| `thisAccount` | `BankAccount` | The account this transaction belongs to | `BankAccount(...)` | -| `otherAccount` | `CounterpartyCore` | The counterparty account | `CounterpartyCore(...)` | -| `transactionType` | `String` | Type of transaction | `"DEBIT"`, `"CREDIT"` | -| `amount` | `BigDecimal` | Transaction amount | `250.00` | -| `currency` | `String` | Currency code | `"GBP"`, `"EUR"`, `"USD"` | -| `description` | `Option[String]` | Transaction description | `Some("Payment to supplier")` or `None` | -| `startDate` | `Date` | Transaction start date | `Date(...)` | -| `finishDate` | `Date` | Transaction completion date | `Date(...)` | -| `balance` | `BigDecimal` | Account balance after transaction | `1234.56` | - -### Example Rules Using transactionOpt - -```scala -// 1. Allow transactions under a limit -transactionOpt.exists(txn => txn.amount.toDouble < 10000.0) - -// 2. Check transaction type -transactionOpt.exists(txn => txn.transactionType == "CREDIT") - -// 3. Check transaction currency -transactionOpt.exists(txn => txn.currency == "GBP") - -// 4. Check transaction description -transactionOpt.exists(txn => - txn.description.exists(desc => desc.contains("salary")) -) - -// 5. Check transaction belongs to account -(transactionOpt, accountOpt) match { - case (Some(txn), Some(account)) => - txn.thisAccount.accountId == account.accountId - case _ => false -} - -// 6. Complex amount and type check -transactionOpt.exists(txn => - txn.amount.toDouble >= 100.0 && - txn.amount.toDouble <= 5000.0 && - txn.transactionType == "DEBIT" -) - -// 7. Check recent transaction (within 30 days) -import java.time.Instant -import java.time.temporal.ChronoUnit - -transactionOpt.exists(txn => { - val now = Instant.now() - val txnInstant = txn.finishDate.toInstant - ChronoUnit.DAYS.between(txnInstant, now) <= 30 -}) -``` - ---- - -## 11. customerOpt (Option[Customer]) - -Optional customer context. Present when `customer_id` is provided in the API request. - -### Available Properties (when present) - -| Property | Type | Description | Example | -|----------|------|-------------|---------| -| `customerId` | `String` | Unique customer identifier (UUID) | `"cust-456-789"` | -| `bankId` | `String` | Bank identifier | `"gh.29.uk"` | -| `number` | `String` | Customer number (bank's identifier) | `"CUST123456"` | -| `legalName` | `String` | Legal name of customer | `"Alice Jane Smith"` | -| `mobileNumber` | `String` | Mobile phone number | `"+44 7700 900000"` | -| `email` | `String` | Email address | `"alice@example.com"` | -| `faceImage` | `CustomerFaceImageTrait` | Face image information | `CustomerFaceImage(...)` | -| `dateOfBirth` | `Date` | Date of birth | `Date(1990, 1, 1)` | -| `relationshipStatus` | `String` | Marital status | `"Single"`, `"Married"` | -| `dependents` | `Integer` | Number of dependents | `2` | -| `dobOfDependents` | `List[Date]` | Dates of birth of dependents | `List(Date(...))` | -| `highestEducationAttained` | `String` | Education level | `"Bachelor's Degree"` | -| `employmentStatus` | `String` | Employment status | `"Employed"`, `"Self-Employed"` | -| `creditRating` | `CreditRatingTrait` | Credit rating information | `CreditRating(...)` | -| `creditLimit` | `AmountOfMoneyTrait` | Credit limit | `AmountOfMoney(...)` | -| `kycStatus` | `Boolean` | KYC verification status | `true` or `false` | -| `lastOkDate` | `Date` | Last OK date | `Date(...)` | -| `title` | `String` | Title | `"Mr"`, `"Ms"`, `"Dr"` | -| `branchId` | `String` | Branch identifier | `"branch-123"` | -| `nameSuffix` | `String` | Name suffix | `"Jr"`, `"III"` | - -### Example Rules Using customerOpt - -```scala -// 1. Check KYC status -customerOpt.exists(customer => customer.kycStatus == true) - -// 2. Check customer belongs to bank -customerOpt.exists(customer => customer.bankId == "gh.29.uk") - -// 3. Check customer age (over 18) -import java.time.LocalDate -import java.time.Period -import java.time.ZoneId - -customerOpt.exists(customer => { - val today = LocalDate.now() - val birthDate = LocalDate.ofInstant(customer.dateOfBirth.toInstant, ZoneId.systemDefault()) - Period.between(birthDate, today).getYears >= 18 -}) - -// 4. Check employment status -customerOpt.exists(customer => - customer.employmentStatus == "Employed" || - customer.employmentStatus == "Self-Employed" -) - -// 5. Check customer email matches user -customerOpt.exists(customer => customer.email == user.emailAddress) - -// 6. Check number of dependents -customerOpt.exists(customer => customer.dependents <= 3) - -// 7. Check education level -customerOpt.exists(customer => - customer.highestEducationAttained.contains("Degree") -) - -// 8. Verify customer and account belong to same bank -(customerOpt, accountOpt) match { - case (Some(customer), Some(account)) => - customer.bankId == account.bankId.value - case _ => false -} - -// 9. Check mobile number is provided -customerOpt.exists(customer => - customer.mobileNumber.nonEmpty && customer.mobileNumber != "" -) -``` - ---- - -## Complex Rule Examples - -### Example 1: Multi-Object Validation - -```scala -// Allow if: -// - Authenticated user is admin, OR -// - Authenticated user has finance department attribute, OR -// - User matches authenticated user AND account has sufficient balance - -val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com") -val isFinance = authenticatedUserAttributes.exists(attr => - attr.name == "department" && attr.value == "finance" -) -val isSelfAccess = user.userId == authenticatedUser.userId -val hasBalance = accountOpt.exists(_.balance.toDouble > 1000.0) - -isAdmin || isFinance || (isSelfAccess && hasBalance) -``` - -### Example 2: Delegation Check with Attributes - -```scala -// Allow if customer service is acting on behalf of someone with proper attributes -val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com") -val hasDelegation = onBehalfOfUserOpt.isDefined -val onBehalfHasRole = onBehalfOfUserAttributes.exists(attr => - attr.name == "role" && List("customer", "premium_customer").contains(attr.value) -) -val onBehalfHasSession = onBehalfOfUserAuthContext.exists(_.key == "session_id") - -isCustomerService && hasDelegation && onBehalfHasRole && onBehalfHasSession -``` - -### Example 3: Transaction Approval Based on Customer - -```scala -// Allow transaction if: -// - Customer is KYC verified AND -// - Transaction is under limit AND -// - Transaction currency matches account - -(customerOpt, transactionOpt, accountOpt) match { - case (Some(customer), Some(txn), Some(account)) => - val isKycVerified = customer.kycStatus == true - val underLimit = txn.amount.toDouble < 10000.0 - val correctCurrency = txn.currency == account.currency - isKycVerified && underLimit && correctCurrency - case _ => false -} -``` - -### Example 4: Bank-Specific Rules - -```scala -// Different rules for different banks -bankOpt match { - case Some(bank) if bank.bankId.value.startsWith("gh.") => - // UK bank rules - require higher balance - accountOpt.exists(_.balance.toDouble > 5000.0) - case Some(bank) if bank.bankId.value.startsWith("us.") => - // US bank rules - require KYC - customerOpt.exists(_.kycStatus == true) - case Some(_) => - // Other banks - basic check - user.emailAddress.nonEmpty - case None => - // No bank context - deny - false -} -``` - ---- - -## Working with Optional Objects - -All objects except `authenticatedUser`, `authenticatedUserAttributes`, `authenticatedUserAuthContext`, `onBehalfOfUserAttributes`, `onBehalfOfUserAuthContext`, and `user` are optional. Here are patterns for working with them: - -### Pattern 1: exists() - -```scala -// Check if bank exists and has a property -bankOpt.exists(bank => bank.bankId.value == "gh.29.uk") -``` - -### Pattern 2: Pattern Matching - -```scala -// Match on multiple objects simultaneously -(bankOpt, accountOpt) match { - case (Some(bank), Some(account)) => - bank.bankId == account.bankId - case _ => false -} -``` - -### Pattern 3: isDefined / isEmpty - -```scala -// Check if object is provided -if (bankOpt.isDefined) { - val bank = bankOpt.get - bank.bankId.value == "gh.29.uk" -} else { - false -} -``` - -### Pattern 4: for Comprehension - -```scala -// Chain multiple optional checks -val result = for { - bank <- bankOpt - account <- accountOpt - if bank.bankId == account.bankId - if account.balance.toDouble > 1000.0 -} yield true - -result.getOrElse(false) -``` - ---- - -## Common Patterns and Best Practices - -### 1. Type Conversions - -```scala -// BigDecimal to Double -account.balance.toDouble - -// Date comparisons -txn.finishDate.before(new Date()) -txn.finishDate.after(new Date()) - -// String to numeric -account.number.toLong -``` - -### 2. String Operations - -```scala -// Case-insensitive comparison -user.emailAddress.toLowerCase == "alice@example.com" - -// Contains check -bank.fullName.contains("Bank") - -// Starts with / Ends with -user.emailAddress.endsWith("@admin.com") -bank.bankId.value.startsWith("gh.") -``` - -### 3. List Operations - -```scala -// Check if list is empty -customer.dobOfDependents.isEmpty - -// Check list size -customer.dobOfDependents.length > 0 - -// Find in list -account.accountRoutings.exists(routing => routing.scheme == "IBAN") -``` - -### 4. Safe Navigation - -```scala -// Use getOrElse for defaults -txn.description.getOrElse("No description") - -// Chain optional operations -txn.description.getOrElse("No description").toLowerCase.contains("payment") -``` - ---- - -## Import Statements Available - -These imports are automatically available in your ABAC rule code: - -```scala -import com.openbankproject.commons.model._ -import code.model.dataAccess.ResourceUser -import net.liftweb.common._ -``` - -You can also use standard Scala/Java imports: - -```scala -import java.time._ -import java.util.Date -import scala.util._ -``` - ---- - -## Summary - -- **authenticatedUser**: Always available - the logged in user -- **authenticatedUserAttributes**: Always available - list of non-personal attributes for authenticated user (may be empty) -- **authenticatedUserAuthContext**: Always available - list of auth context for authenticated user (may be empty) -- **onBehalfOfUserOpt**: Optional - present when delegation is used -- **onBehalfOfUserAttributes**: Always available - list of non-personal attributes for onBehalfOf user (empty if no delegation) -- **onBehalfOfUserAuthContext**: Always available - list of auth context for onBehalfOf user (empty if no delegation) -- **user**: Always available - a user object -- **bankOpt, accountOpt, transactionOpt, customerOpt**: Optional - use `.exists()` or pattern matching -- **Type conversions**: Remember `.toDouble` for BigDecimal, `.value` for ID types -- **Safe access**: Use `getOrElse()` for Option fields -- **Build incrementally**: Break complex rules into named parts -- **READ-ONLY**: All objects are read-only - you cannot modify them in rules - ---- - -**Last Updated:** 2024 -**Related Documentation:** ABAC_SIMPLE_GUIDE.md, ABAC_REFACTORING.md, ABAC_TESTING_EXAMPLES.md \ No newline at end of file diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md b/obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md deleted file mode 100644 index 73cb3f962..000000000 --- a/obp-api/src/main/resources/docs/glossary/ABAC_Parameters_Summary.md +++ /dev/null @@ -1,267 +0,0 @@ -# ABAC Rule Parameters - Complete Reference - -This document lists all 16 parameters available in ABAC (Attribute-Based Access Control) rules. - -## Overview - -ABAC rules receive **18 parameters** that provide complete context for access control decisions. - -**All parameters are READ-ONLY** - you can only read and evaluate, never modify. - -## Complete Parameter List - -| # | Parameter | Type | Always Available? | Description | -|---|-----------|------|-------------------|-------------| -| 1 | `authenticatedUser` | `User` | ✅ Yes | The user who is logged in and making the API call | -| 2 | `authenticatedUserAttributes` | `List[UserAttribute]` | ✅ Yes | Non-personal attributes for the authenticated user (may be empty) | -| 3 | `authenticatedUserAuthContext` | `List[UserAuthContext]` | ✅ Yes | Auth context for the authenticated user (may be empty) | -| 4 | `onBehalfOfUserOpt` | `Option[User]` | ❌ Optional | User being represented in delegation scenarios | -| 5 | `onBehalfOfUserAttributes` | `List[UserAttribute]` | ✅ Yes | Non-personal attributes for onBehalfOf user (empty if no delegation) | -| 6 | `onBehalfOfUserAuthContext` | `List[UserAuthContext]` | ✅ Yes | Auth context for onBehalfOf user (empty if no delegation) | -| 7 | `userOpt` | `Option[User]` | ❌ Optional | A user object (when user_id is provided) | -| 8 | `userAttributes` | `List[UserAttribute]` | ✅ Yes | Non-personal attributes for user (empty if no user) | -| 9 | `bankOpt` | `Option[Bank]` | ❌ Optional | Bank object (when bank_id is provided) | -| 10 | `bankAttributes` | `List[BankAttributeTrait]` | ✅ Yes | Attributes for bank (empty if no bank) | -| 11 | `accountOpt` | `Option[BankAccount]` | ❌ Optional | Account object (when account_id is provided) | -| 12 | `accountAttributes` | `List[AccountAttribute]` | ✅ Yes | Attributes for account (empty if no account) | -| 13 | `transactionOpt` | `Option[Transaction]` | ❌ Optional | Transaction object (when transaction_id is provided) | -| 14 | `transactionAttributes` | `List[TransactionAttribute]` | ✅ Yes | Attributes for transaction (empty if no transaction) | -| 15 | `transactionRequestOpt` | `Option[TransactionRequest]` | ❌ Optional | Transaction request object (when transaction_request_id is provided) | -| 16 | `transactionRequestAttributes` | `List[TransactionRequestAttributeTrait]` | ✅ Yes | Attributes for transaction request (empty if no transaction request) | -| 17 | `customerOpt` | `Option[Customer]` | ❌ Optional | Customer object (when customer_id is provided) | -| 18 | `customerAttributes` | `List[CustomerAttribute]` | ✅ Yes | Attributes for customer (empty if no customer) | - -## Function Signature - -```scala -type AbacRuleFunction = ( - User, // 1. authenticatedUser - List[UserAttribute], // 2. authenticatedUserAttributes - List[UserAuthContext], // 3. authenticatedUserAuthContext - Option[User], // 4. onBehalfOfUserOpt - List[UserAttribute], // 5. onBehalfOfUserAttributes - List[UserAuthContext], // 6. onBehalfOfUserAuthContext - Option[User], // 7. userOpt - List[UserAttribute], // 8. userAttributes - Option[Bank], // 9. bankOpt - List[BankAttributeTrait], // 10. bankAttributes - Option[BankAccount], // 11. accountOpt - List[AccountAttribute], // 12. accountAttributes - Option[Transaction], // 13. transactionOpt - List[TransactionAttribute], // 14. transactionAttributes - Option[TransactionRequest], // 15. transactionRequestOpt - List[TransactionRequestAttributeTrait], // 16. transactionRequestAttributes - Option[Customer], // 17. customerOpt - List[CustomerAttribute] // 18. customerAttributes -) => Boolean -``` - -## Parameter Groups - -### Group 1: Authenticated User (Always Available) -- `authenticatedUser` - The logged in user -- `authenticatedUserAttributes` - Their non-personal attributes -- `authenticatedUserAuthContext` - Their auth context (session, IP, etc.) - -### Group 2: OnBehalfOf User (Delegation) -- `onBehalfOfUserOpt` - Optional delegated user -- `onBehalfOfUserAttributes` - Their non-personal attributes (empty if no delegation) -- `onBehalfOfUserAuthContext` - Their auth context (empty if no delegation) - -### Group 3: Target User (Optional) -- `userOpt` - Optional user object -- `userAttributes` - Their non-personal attributes (empty if no user) - -### Group 4: Bank (Optional) -- `bankOpt` - Optional bank object -- `bankAttributes` - Bank attributes (empty if no bank) - -### Group 5: Account (Optional) -- `accountOpt` - Optional account object -- `accountAttributes` - Account attributes (empty if no account) - -### Group 6: Transaction (Optional) -- `transactionOpt` - Optional transaction object -- `transactionAttributes` - Transaction attributes (empty if no transaction) - -### Group 7: Transaction Request (Optional) -- `transactionRequestOpt` - Optional transaction request object -- `transactionRequestAttributes` - Transaction request attributes (empty if no transaction request) - -### Group 8: Customer (Optional) -- `customerOpt` - Optional customer object -- `customerAttributes` - Customer attributes (empty if no customer) - -## Example Rules - -### Example 1: Check Authenticated User Attribute -```scala -authenticatedUserAttributes.exists(attr => - attr.name == "department" && attr.value == "finance" -) -``` - -### Example 2: Check Bank Attribute -```scala -bankAttributes.exists(attr => - attr.name == "country" && attr.value == "UK" -) -``` - -### Example 3: Check Account Attribute -```scala -accountAttributes.exists(attr => - attr.name == "account_type" && attr.value == "premium" -) -``` - -### Example 4: Check Transaction Attribute -```scala -transactionAttributes.exists(attr => - attr.name == "risk_score" && - attr.value.toIntOption.exists(_ < 5) -) -``` - -### Example 5: Check Transaction Request Attribute -```scala -transactionRequestAttributes.exists(attr => - attr.name == "approval_status" && attr.value == "pending" -) -``` - -### Example 6: Check Customer Attribute -```scala -customerAttributes.exists(attr => - attr.name == "kyc_status" && attr.value == "verified" -) -``` - -### Example 7: Complex Multi-Attribute Rule -```scala -// Allow if: -// - Authenticated user is in finance department -// - Bank is in allowed countries -// - Account is premium -// - Transaction risk is low - -val authIsFinance = authenticatedUserAttributes.exists(attr => - attr.name == "department" && attr.value == "finance" -) - -val bankAllowed = bankAttributes.exists(attr => - attr.name == "country" && List("UK", "US", "DE").contains(attr.value) -) - -val accountPremium = accountAttributes.exists(attr => - attr.name == "account_type" && attr.value == "premium" -) - -val lowRisk = transactionAttributes.exists(attr => - attr.name == "risk_score" && attr.value.toIntOption.exists(_ < 3) -) - -authIsFinance && bankAllowed && accountPremium && lowRisk -``` - -### Example 8: Delegation with Attributes -```scala -// Allow customer service to help premium customers -val isCustomerService = authenticatedUserAttributes.exists(attr => - attr.name == "role" && attr.value == "customer_service" -) - -val hasDelegation = onBehalfOfUserOpt.isDefined - -val customerIsPremium = onBehalfOfUserAttributes.exists(attr => - attr.name == "customer_tier" && attr.value == "premium" -) - -isCustomerService && hasDelegation && customerIsPremium -``` - -## API Request Mapping - -When you make an API request: - -```json -{ - "authenticated_user_id": "alice@example.com", - "on_behalf_of_user_id": "bob@example.com", - "user_id": "charlie@example.com", - "bank_id": "gh.29.uk", - "account_id": "acc-123", - "transaction_id": "txn-456", - "transaction_request_id": "tr-123", - "customer_id": "cust-789" -} -``` - -The engine automatically: -1. Fetches `authenticatedUser` using `authenticated_user_id` (or from auth token if not provided) -2. Fetches `authenticatedUserAttributes` and `authenticatedUserAuthContext` for authenticated user -3. Fetches `onBehalfOfUserOpt`, `onBehalfOfUserAttributes`, `onBehalfOfUserAuthContext` if `on_behalf_of_user_id` provided -4. Fetches `userOpt` and `userAttributes` if `user_id` provided -5. Fetches `bankOpt` and `bankAttributes` if `bank_id` provided -6. Fetches `accountOpt` and `accountAttributes` if `account_id` provided -7. Fetches `transactionOpt` and `transactionAttributes` if `transaction_id` provided -8. Fetches `transactionRequestOpt` and `transactionRequestAttributes` if `transaction_request_id` provided -9. Fetches `customerOpt` and `customerAttributes` if `customer_id` provided - -## Working with Attributes - -All attribute lists follow the same pattern: - -```scala -// Check if attribute exists with specific value -attributeList.exists(attr => attr.name == "key" && attr.value == "value") - -// Check if list is empty -attributeList.isEmpty - -// Check if list has any attributes -attributeList.nonEmpty - -// Find specific attribute -attributeList.find(_.name == "key").map(_.value) - -// Multiple attributes (AND) -val hasAttr1 = attributeList.exists(_.name == "key1") -val hasAttr2 = attributeList.exists(_.name == "key2") -hasAttr1 && hasAttr2 - -// Multiple attributes (OR) -attributeList.exists(attr => - List("key1", "key2", "key3").contains(attr.name) -) -``` - -## Key Points - -✅ **18 parameters total** - comprehensive context for access decisions -✅ **3 always available objects** - authenticatedUser, authenticatedUserAttributes, authenticatedUserAuthContext -✅ **15 contextual parameters** - available based on what IDs are provided in the request -✅ **All READ-ONLY** - cannot modify any parameter values -✅ **Automatic fetching** - engine fetches all data based on provided IDs -✅ **Type safety** - optional objects use `Option[T]`, lists are `List[T]` -✅ **Empty lists not None** - attribute lists are always available, just empty when no data - -## Summary - -ABAC rules have access to: -- **3 user contexts**: authenticated, onBehalfOf, and target user -- **5 resource contexts**: bank, account, transaction, transaction request, customer -- **Complete attribute data**: for all users and resources -- **Auth context**: session, IP, device info, etc. -- **Full type safety**: optional objects and guaranteed lists - -This provides everything needed to make sophisticated access control decisions! - ---- - -**Related Documentation:** -- `ABAC_OBJECT_PROPERTIES_REFERENCE.md` - Detailed property reference for each object -- `ABAC_SIMPLE_GUIDE.md` - Getting started guide -- `ABAC_REFACTORING.md` - Technical implementation details - -**Last Updated:** 2024 \ No newline at end of file diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md b/obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md deleted file mode 100644 index e4815df12..000000000 --- a/obp-api/src/main/resources/docs/glossary/ABAC_Simple_Guide.md +++ /dev/null @@ -1,354 +0,0 @@ -# ABAC Rules Engine - Simple Guide - -## Overview - -The ABAC (Attribute-Based Access Control) Rules Engine allows you to create dynamic access control rules in Scala that evaluate whether a user should have access to a resource. - -## Core Concept - -**One Rule + One Execution Method = Simple Access Control** - -```scala -def executeRule( - ruleId: String, - authenticatedUserId: String, - onBehalfOfUserId: Option[String] = None, - userId: Option[String] = None, - callContext: Option[CallContext] = None, - bankId: Option[String] = None, - accountId: Option[String] = None, - viewId: Option[String] = None, - transactionId: Option[String] = None, - customerId: Option[String] = None -): Box[Boolean] -``` - ---- - -## Understanding the Three User Parameters - -### 1. `authenticatedUserId` (Required) -**The person actually logged in and making the API call** - -- This is ALWAYS the real user who authenticated -- Retrieved from the authentication token -- Cannot be faked or changed - -**Example:** Alice logs into the banking app -- `authenticatedUserId = "alice@example.com"` - ---- - -### 2. `onBehalfOfUserId` (Optional) -**When someone acts on behalf of another user (delegation)** - -- Used for delegation scenarios -- The authenticated user is acting for someone else -- Common in customer service, admin tools, power of attorney - -**Example:** Customer service rep Bob helps Alice with her account -- `authenticatedUserId = "bob@customerservice.com"` (the rep logged in) -- `onBehalfOfUserId = "alice@example.com"` (helping Alice) -- `userId = "alice@example.com"` (checking Alice's permissions) - ---- - -### 3. `userId` (Optional) -**The target user being evaluated by the rule** - -- Defaults to `authenticatedUserId` if not provided -- The user whose permissions/attributes are being checked -- Useful for testing rules for different users - -**Example:** Admin checking if Alice can access an account -- `authenticatedUserId = "admin@example.com"` (admin is logged in) -- `userId = "alice@example.com"` (checking Alice's access) - ---- - -## Common Scenarios - -### Scenario 1: Normal User Access -**Alice wants to view her own account** - -```json -{ - "bank_id": "gh.29.uk", - "account_id": "alice-account-123" -} -``` - -Behind the scenes: -- `authenticatedUserId = "alice@example.com"` (from auth token) -- `onBehalfOfUserId = None` -- `userId = None` → defaults to Alice - -**Rule example:** -```scala -// Check if user owns the account -accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) -``` - ---- - -### Scenario 2: Customer Service Delegation -**Bob (customer service) helps Alice view her account** - -```json -{ - "on_behalf_of_user_id": "alice@example.com", - "bank_id": "gh.29.uk", - "account_id": "alice-account-123" -} -``` - -Behind the scenes: -- `authenticatedUserId = "bob@customerservice.com"` (from auth token) -- `onBehalfOfUserId = "alice@example.com"` -- `userId = None` → defaults to Bob, but rule can check both - -**Rule example:** -```scala -// Allow if authenticated user is customer service AND acting on behalf of an account owner -val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com") -val hasValidDelegation = onBehalfOfUserOpt.isDefined -val targetOwnsAccount = accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) - -isCustomerService && hasValidDelegation && targetOwnsAccount -``` - ---- - -### Scenario 3: Admin Testing -**Admin wants to test if Alice can access an account (without logging in as Alice)** - -```json -{ - "user_id": "alice@example.com", - "bank_id": "gh.29.uk", - "account_id": "alice-account-123" -} -``` - -Behind the scenes: -- `authenticatedUserId = "admin@example.com"` (from auth token) -- `onBehalfOfUserId = None` -- `userId = "alice@example.com"` (evaluating for Alice) - -**Rule example:** -```scala -// Allow admins to test access, or allow if user owns account -val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com") -val userOwnsAccount = accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) - -isAdmin || userOwnsAccount -``` - ---- - -## API Usage - -### Endpoint -``` -POST /obp/v6.0.0/management/abac-rules/{RULE_ID}/execute -``` - -### Request Examples - -#### Example 1: Basic Access Check -```json -{ - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" -} -``` -- Checks if authenticated user can access the account - -#### Example 2: Delegation -```json -{ - "on_behalf_of_user_id": "alice@example.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" -} -``` -- Authenticated user acting on behalf of Alice - -#### Example 3: Testing for Different User -```json -{ - "user_id": "bob@example.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" -} -``` -- Check if Bob can access the account (useful for admins testing) - -#### Example 4: Complex Scenario -```json -{ - "on_behalf_of_user_id": "alice@example.com", - "user_id": "charlie@example.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - "transaction_id": "trans-123" -} -``` -- Authenticated user acting on behalf of Alice -- Checking if Charlie can access account and transaction - ---- - -## Writing ABAC Rules - -### Available Objects in Rules - -```scala -// These are available in your rule code: -authenticatedUser: User // Always present - the logged in user -onBehalfOfUserOpt: Option[User] // Present if delegation -user: User // Always present - the target user being evaluated -bankOpt: Option[Bank] // Present if bank_id provided -accountOpt: Option[BankAccount] // Present if account_id provided -transactionOpt: Option[Transaction] // Present if transaction_id provided -customerOpt: Option[Customer] // Present if customer_id provided -``` - -### Simple Rule Examples - -#### Rule 1: User Must Own Account -```scala -accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) -``` - -#### Rule 2: Admin or Owner -```scala -val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com") -val isOwner = accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) - -isAdmin || isOwner -``` - -#### Rule 3: Customer Service Delegation -```scala -val isCustomerService = authenticatedUser.emailAddress.contains("@customerservice.com") -val actingOnBehalf = onBehalfOfUserOpt.isDefined -val userIsOwner = accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) - -// Allow if customer service is helping an account owner -isCustomerService && actingOnBehalf && userIsOwner -``` - -#### Rule 4: Self-Service Only (No Delegation) -```scala -// User must be checking their own access (no delegation allowed) -val isSelfService = authenticatedUser.userId == user.userId -val noDelegation = onBehalfOfUserOpt.isEmpty - -isSelfService && noDelegation -``` - -#### Rule 5: Account Balance Check -```scala -accountOpt.exists(account => account.balance.toDouble >= 1000.0) -``` - ---- - -## Quick Reference Table - -| Parameter | Required? | Purpose | Example Value | -|-----------|-----------|---------|---------------| -| `authenticatedUserId` | ✅ Yes | Who is logged in | `"alice@example.com"` | -| `onBehalfOfUserId` | ❌ Optional | Delegation | `"bob@example.com"` | -| `userId` | ❌ Optional | Target user to evaluate | `"charlie@example.com"` | -| `bankId` | ❌ Optional | Bank context | `"gh.29.uk"` | -| `accountId` | ❌ Optional | Account context | `"acc-123"` | -| `viewId` | ❌ Optional | View context | `"owner"` | -| `transactionId` | ❌ Optional | Transaction context | `"trans-456"` | -| `customerId` | ❌ Optional | Customer context | `"cust-789"` | - ---- - -## Real-World Use Cases - -### Use Case 1: Personal Banking -- User logs in → `authenticatedUserId` -- Views their own account → `userId` defaults to authenticated user -- Rule checks ownership - -### Use Case 2: Business Banking with Delegates -- CFO logs in → `authenticatedUserId = "cfo@company.com"` -- Checks on behalf of CEO → `onBehalfOfUserId = "ceo@company.com"` -- System evaluates if CEO has access → `userId = "ceo@company.com"` - -### Use Case 3: Customer Support -- Support agent logs in → `authenticatedUserId = "agent@bank.com"` -- Helps customer → `onBehalfOfUserId = "customer@example.com"` -- Rule verifies: agent has support role AND customer owns account - -### Use Case 4: Admin Panel -- Admin logs in → `authenticatedUserId = "admin@bank.com"` -- Tests rule for any user → `userId = "testuser@example.com"` -- Rule evaluates for test user, but admin must be authenticated - ---- - -## Testing Tips - -### Test Different Users -```bash -# Test as yourself -curl -X POST .../execute -d '{"bank_id": "gh.29.uk"}' - -# Test for another user (if you have permission) -curl -X POST .../execute -d '{"user_id": "other@example.com", "bank_id": "gh.29.uk"}' -``` - -### Test Delegation -```bash -# Act on behalf of someone -curl -X POST .../execute -d '{ - "on_behalf_of_user_id": "alice@example.com", - "bank_id": "gh.29.uk" -}' -``` - -### Debug Your Rules -```scala -// Add simple checks to understand what's happening -val result = (authenticatedUser.userId == user.userId) -println(s"Auth user: ${authenticatedUser.userId}, Target user: ${user.userId}, Match: $result") -result -``` - ---- - -## Summary - -✅ **Keep it simple**: One execution method, clear parameters -✅ **Three user IDs**: authenticated (who), on-behalf-of (delegation), user (target) -✅ **Write rules in Scala**: Full power of the language -✅ **Test via API**: Just pass IDs, objects fetched automatically -✅ **Flexible**: Supports normal access, delegation, and admin testing - ---- - -**Related Documentation:** -- `ABAC_OBJECT_PROPERTIES_REFERENCE.md` - Full list of available properties -- `ABAC_TESTING_EXAMPLES.md` - More testing examples -- `ABAC_REFACTORING.md` - Technical implementation details - -**Last Updated:** 2024 \ No newline at end of file diff --git a/obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md b/obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md deleted file mode 100644 index b1a64564f..000000000 --- a/obp-api/src/main/resources/docs/glossary/ABAC_Testing_Examples.md +++ /dev/null @@ -1,622 +0,0 @@ -# ABAC Rule Testing Examples - -This document provides practical examples for testing ABAC (Attribute-Based Access Control) rules using the refactored ID-based API. - -## Prerequisites - -1. You need a valid DirectLogin token or other authentication method -2. You must have the `canExecuteAbacRule` entitlement -3. You need to know the IDs of: - - ABAC rules you want to test - - Users, banks, accounts, transactions, customers (as needed by your rules) - -## API Endpoint - -``` -POST /obp/v6.0.0/management/abac-rules/{RULE_ID}/execute -``` - -## Basic Examples - -### Example 1: Simple User-Only Rule - -Test a rule that only checks user attributes (no bank/account context needed). - -**Rule Code:** -```scala -// Rule: Only allow admin users -user.userId.endsWith("@admin.com") -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/admin-only-rule/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{}' -``` - -**Response:** -```json -{ - "rule_id": "admin-only-rule", - "rule_name": "Admin Only Access", - "result": true, - "message": "Access granted" -} -``` - -### Example 2: Test Rule for Different User - -Test how the rule behaves for a different user (without re-authenticating). - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/admin-only-rule/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "user_id": "alice@example.com" - }' -``` - -**Response:** -```json -{ - "rule_id": "admin-only-rule", - "rule_name": "Admin Only Access", - "result": false, - "message": "Access denied" -} -``` - -### Example 3: Bank-Specific Rule - -Test a rule that checks bank context. - -**Rule Code:** -```scala -// Rule: Only allow access to UK banks -bankOpt.exists(bank => - bank.bankId.value.startsWith("gh.") || - bank.bankId.value.startsWith("uk.") -) -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/uk-banks-only/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "bank_id": "gh.29.uk" - }' -``` - -**Response:** -```json -{ - "rule_id": "uk-banks-only", - "rule_name": "UK Banks Only", - "result": true, - "message": "Access granted" -} -``` - -### Example 4: Account Balance Rule - -Test a rule that checks account balance. - -**Rule Code:** -```scala -// Rule: Only allow if account balance > 1000 -accountOpt.exists(account => - account.balance.toDouble > 1000.0 -) -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/high-balance-only/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" - }' -``` - -**Response:** -```json -{ - "rule_id": "high-balance-only", - "rule_name": "High Balance Only", - "result": true, - "message": "Access granted" -} -``` - -### Example 5: Account Ownership Rule - -Test a rule that checks if user owns the account. - -**Rule Code:** -```scala -// Rule: User must own the account -accountOpt.exists(account => - account.owners.exists(owner => owner.userId == user.userId) -) -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/account-owner-only/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "user_id": "alice@example.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" - }' -``` - -### Example 6: Transaction Amount Rule - -Test a rule that checks transaction amount. - -**Rule Code:** -```scala -// Rule: Only allow transactions under 10000 -transactionOpt.exists(txn => - txn.amount.toDouble < 10000.0 -) -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/small-transactions/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - "transaction_id": "trans-123" - }' -``` - -### Example 7: Customer Credit Rating Rule - -Test a rule that checks customer credit rating. - -**Rule Code:** -```scala -// Rule: Only allow customers with excellent credit -customerOpt.exists(customer => - customer.creditRating.getOrElse("") == "EXCELLENT" -) -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/excellent-credit-only/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "bank_id": "gh.29.uk", - "customer_id": "cust-456" - }' -``` - -## Complex Examples - -### Example 8: Multi-Condition Rule - -Test a complex rule with multiple conditions. - -**Rule Code:** -```scala -// Rule: Allow if: -// - User is admin, OR -// - User owns account AND balance > 100 AND account is at UK bank -val isAdmin = user.userId.endsWith("@admin.com") -val ownsAccount = accountOpt.exists(_.owners.exists(_.userId == user.userId)) -val hasBalance = accountOpt.exists(_.balance.toDouble > 100.0) -val isUKBank = bankOpt.exists(b => - b.bankId.value.startsWith("gh.") || b.bankId.value.startsWith("uk.") -) - -isAdmin || (ownsAccount && hasBalance && isUKBank) -``` - -**Test Request (Admin User):** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/complex-access/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "user_id": "admin@admin.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" - }' -``` - -**Test Request (Regular User):** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/complex-access/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "user_id": "alice@example.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" - }' -``` - -### Example 9: Time-Based Rule - -Test a rule that includes time-based logic. - -**Rule Code:** -```scala -// Rule: Only allow during business hours (9 AM - 5 PM) unless user is admin -import java.time.LocalTime -import java.time.ZoneId - -val now = LocalTime.now(ZoneId.of("Europe/London")) -val isBusinessHours = now.isAfter(LocalTime.of(9, 0)) && now.isBefore(LocalTime.of(17, 0)) -val isAdmin = user.userId.endsWith("@admin.com") - -isAdmin || isBusinessHours -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/business-hours-only/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "user_id": "alice@example.com" - }' -``` - -### Example 10: Cross-Entity Validation - -Test a rule that validates relationships between entities. - -**Rule Code:** -```scala -// Rule: Customer must be associated with the same bank as the account -(customerOpt, accountOpt, bankOpt) match { - case (Some(customer), Some(account), Some(bank)) => - customer.bankId == bank.bankId && - account.bankId == bank.bankId - case _ => false -} -``` - -**Test Request:** -```bash -curl -X POST \ - 'https://api.openbankproject.com/obp/v6.0.0/management/abac-rules/cross-entity-validation/execute' \ - -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \ - -H 'Content-Type: application/json' \ - -d '{ - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - "customer_id": "cust-456" - }' -``` - -## Testing Patterns - -### Pattern 1: Test Multiple Users - -Test the same rule for different users to verify behavior: - -```bash -# Test for admin -curl -X POST 'https://.../execute' -d '{"user_id": "admin@admin.com", "bank_id": "gh.29.uk"}' - -# Test for regular user -curl -X POST 'https://.../execute' -d '{"user_id": "alice@example.com", "bank_id": "gh.29.uk"}' - -# Test for another user -curl -X POST 'https://.../execute' -d '{"user_id": "bob@example.com", "bank_id": "gh.29.uk"}' -``` - -### Pattern 2: Test Different Banks - -Test how the rule behaves across different banks: - -```bash -# UK Bank -curl -X POST 'https://.../execute' -d '{"bank_id": "gh.29.uk", "account_id": "acc1"}' - -# US Bank -curl -X POST 'https://.../execute' -d '{"bank_id": "us.bank.01", "account_id": "acc2"}' - -# German Bank -curl -X POST 'https://.../execute' -d '{"bank_id": "de.bank.01", "account_id": "acc3"}' -``` - -### Pattern 3: Test Edge Cases - -Test boundary conditions: - -```bash -# No context (minimal) -curl -X POST 'https://.../execute' -d '{}' - -# Partial context -curl -X POST 'https://.../execute' -d '{"bank_id": "gh.29.uk"}' - -# Full context -curl -X POST 'https://.../execute' -d '{ - "user_id": "alice@example.com", - "bank_id": "gh.29.uk", - "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", - "transaction_id": "trans-123", - "customer_id": "cust-456" -}' - -# Invalid IDs (should handle gracefully) -curl -X POST 'https://.../execute' -d '{"bank_id": "invalid-bank-id"}' -``` - -### Pattern 4: Automated Testing Script - -Create a bash script to test multiple scenarios: - -```bash -#!/bin/bash - -API_BASE="https://api.openbankproject.com/obp/v6.0.0" -TOKEN="eyJhbGciOiJIUzI1..." -RULE_ID="my-test-rule" - -test_rule() { - local description=$1 - local payload=$2 - - echo "Testing: $description" - curl -s -X POST \ - "$API_BASE/management/abac-rules/$RULE_ID/execute" \ - -H "Authorization: DirectLogin token=$TOKEN" \ - -H "Content-Type: application/json" \ - -d "$payload" | jq '.result, .message' - echo "---" -} - -# Run tests -test_rule "Admin user" '{"user_id": "admin@admin.com"}' -test_rule "Regular user" '{"user_id": "alice@example.com"}' -test_rule "With bank context" '{"user_id": "alice@example.com", "bank_id": "gh.29.uk"}' -test_rule "With account context" '{"user_id": "alice@example.com", "bank_id": "gh.29.uk", "account_id": "acc1"}' -``` - -## Error Scenarios - -### Error 1: Rule Not Found - -```bash -curl -X POST 'https://.../management/abac-rules/nonexistent-rule/execute' \ - -H 'Authorization: DirectLogin token=...' \ - -d '{}' -``` - -**Response:** -```json -{ - "code": 404, - "message": "ABAC Rule not found with ID: nonexistent-rule" -} -``` - -### Error 2: Inactive Rule - -If the rule exists but is not active: - -**Response:** -```json -{ - "rule_id": "inactive-rule", - "rule_name": "Inactive Rule", - "result": false, - "message": "Execution error: ABAC Rule Inactive Rule is not active" -} -``` - -### Error 3: Invalid User ID - -```bash -curl -X POST 'https://.../execute' \ - -H 'Authorization: DirectLogin token=...' \ - -d '{"user_id": "nonexistent-user"}' -``` - -**Response:** -```json -{ - "rule_id": "test-rule", - "rule_name": "Test Rule", - "result": false, - "message": "Execution error: User not found" -} -``` - -### Error 4: Compilation Error - -If the rule has invalid Scala code: - -**Response:** -```json -{ - "rule_id": "broken-rule", - "rule_name": "Broken Rule", - "result": false, - "message": "Execution error: Failed to compile ABAC rule: ..." -} -``` - -## Python Testing Example - -```python -import requests -import json - -class AbacRuleTester: - def __init__(self, base_url, token): - self.base_url = base_url - self.headers = { - 'Authorization': f'DirectLogin token={token}', - 'Content-Type': 'application/json' - } - - def test_rule(self, rule_id, **context): - """Test an ABAC rule with given context""" - url = f"{self.base_url}/management/abac-rules/{rule_id}/execute" - - # Filter out None values - payload = {k: v for k, v in context.items() if v is not None} - - response = requests.post(url, headers=self.headers, json=payload) - return response.json() - - def test_users(self, rule_id, user_ids, **context): - """Test rule for multiple users""" - results = {} - for user_id in user_ids: - result = self.test_rule(rule_id, user_id=user_id, **context) - results[user_id] = result['result'] - return results - -# Usage -tester = AbacRuleTester( - base_url='https://api.openbankproject.com/obp/v6.0.0', - token='your-token-here' -) - -# Test single rule -result = tester.test_rule( - 'admin-only-rule', - user_id='alice@example.com', - bank_id='gh.29.uk' -) -print(f"Result: {result['result']}, Message: {result['message']}") - -# Test multiple users -users = ['admin@admin.com', 'alice@example.com', 'bob@example.com'] -results = tester.test_users('account-owner-rule', users, - bank_id='gh.29.uk', - account_id='acc123') -print(results) -# Output: {'admin@admin.com': True, 'alice@example.com': False, ...} -``` - -## JavaScript Testing Example - -```javascript -class AbacRuleTester { - constructor(baseUrl, token) { - this.baseUrl = baseUrl; - this.headers = { - 'Authorization': `DirectLogin token=${token}`, - 'Content-Type': 'application/json' - }; - } - - async testRule(ruleId, context = {}) { - const url = `${this.baseUrl}/management/abac-rules/${ruleId}/execute`; - - // Remove undefined values - const payload = Object.fromEntries( - Object.entries(context).filter(([_, v]) => v !== undefined) - ); - - const response = await fetch(url, { - method: 'POST', - headers: this.headers, - body: JSON.stringify(payload) - }); - - return await response.json(); - } - - async testUsers(ruleId, userIds, context = {}) { - const results = {}; - for (const userId of userIds) { - const result = await this.testRule(ruleId, { ...context, user_id: userId }); - results[userId] = result.result; - } - return results; - } -} - -// Usage -const tester = new AbacRuleTester( - 'https://api.openbankproject.com/obp/v6.0.0', - 'your-token-here' -); - -// Test single rule -const result = await tester.testRule('admin-only-rule', { - user_id: 'alice@example.com', - bank_id: 'gh.29.uk' -}); -console.log(`Result: ${result.result}, Message: ${result.message}`); - -// Test multiple users -const users = ['admin@admin.com', 'alice@example.com', 'bob@example.com']; -const results = await tester.testUsers('account-owner-rule', users, { - bank_id: 'gh.29.uk', - account_id: 'acc123' -}); -console.log(results); -``` - -## Best Practices - -1. **Start Simple**: Begin with rules that only check user attributes, then add complexity -2. **Test Edge Cases**: Always test with missing IDs, invalid IDs, and partial context -3. **Test Multiple Users**: Verify rule behavior for different user types (admin, owner, guest) -4. **Use Automation**: Create scripts to test multiple scenarios quickly -5. **Document Expected Behavior**: Keep track of what each test should return -6. **Test Both Paths**: Test cases that should allow access AND cases that should deny -7. **Performance Testing**: Test with realistic data volumes to ensure rules perform well - -## Troubleshooting - -### Rule Always Returns False - -- Check if the rule is active (`is_active: true`) -- Verify the rule code compiles successfully -- Ensure all required context IDs are provided -- Check if objects are being fetched successfully - -### Rule Times Out - -- Rule execution has a 5-second timeout for object fetching -- Simplify rule logic or optimize database queries -- Consider caching frequently accessed objects - -### Unexpected Results - -- Test with `executeRuleWithObjects` to verify rule logic -- Check object availability (might be `None` if fetch fails) -- Add logging to rule code to debug decision logic -- Verify IDs are correct and objects exist in database - ---- - -**Last Updated:** 2024 -**Related Documentation:** ABAC_REFACTORING.md \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 4de1b85b2..5f11ca078 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -3903,6 +3903,468 @@ object Glossary extends MdcLoggable { |""".stripMargin) + glossaryItems += GlossaryItem( + title = "ABAC_Simple_Guide", + description = + s""" + |# ABAC Rules Engine - Simple Guide + | + |## Overview + | + |The ABAC (Attribute-Based Access Control) Rules Engine allows you to create dynamic access control rules in Scala that evaluate whether a user should have access to a resource. + | + |## API Usage + | + |### Endpoint + |``` + |POST $getObpApiRoot/v6.0.0/management/abac-rules/{RULE_ID}/execute + |``` + | + |### Request Example + |```bash + |curl -X POST \\ + | '$getObpApiRoot/v6.0.0/management/abac-rules/admin-only-rule/execute' \\ + | -H 'Authorization: DirectLogin token=eyJhbGciOiJIUzI1...' \\ + | -H 'Content-Type: application/json' \\ + | -d '{ + | "bank_id": "gh.29.uk", + | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" + | }' + |``` + | + |## Understanding the Three User Parameters + | + |### 1. `authenticatedUserId` (Required) + |**The person actually logged in and making the API call** + | + |- This is ALWAYS the real user who authenticated + |- Retrieved from the authentication token + |- Cannot be faked or changed + | + |### 2. `onBehalfOfUserId` (Optional) + |**When someone acts on behalf of another user (delegation)** + | + |- Used for delegation scenarios + |- The authenticated user is acting for someone else + |- Common in customer service, admin tools, power of attorney + | + |### 3. `userId` (Optional) + |**The target user being evaluated by the rule** + | + |- Defaults to `authenticatedUserId` if not provided + |- The user whose permissions/attributes are being checked + |- Useful for testing rules for different users + | + |## Writing ABAC Rules + | + |### Simple Rule Examples + | + |**Rule 1: User Must Own Account** + |```scala + |accountOpt.exists(account => + | account.owners.exists(owner => owner.userId == user.userId) + |) + |``` + | + |**Rule 2: Admin or Owner** + |```scala + |val isAdmin = authenticatedUser.emailAddress.endsWith("@admin.com") + |val isOwner = accountOpt.exists(account => + | account.owners.exists(owner => owner.userId == user.userId) + |) + | + |isAdmin || isOwner + |``` + | + |**Rule 3: Account Balance Check** + |```scala + |accountOpt.exists(account => account.balance.toDouble >= 1000.0) + |``` + | + |## Available Objects in Rules + | + |```scala + |authenticatedUser: User // Always present - the logged in user + |onBehalfOfUserOpt: Option[User] // Present if delegation + |user: User // Always present - the target user + |bankOpt: Option[Bank] // Present if bank_id provided + |accountOpt: Option[BankAccount] // Present if account_id provided + |transactionOpt: Option[Transaction] // Present if transaction_id provided + |customerOpt: Option[Customer] // Present if customer_id provided + |``` + | + |**Related Documentation:** + |- ABAC_Parameters_Summary - Complete list of all 18 parameters + |- ABAC_Object_Properties_Reference - Detailed property reference + |- ABAC_Testing_Examples - More testing examples + |""".stripMargin) + + glossaryItems += GlossaryItem( + title = "ABAC_Parameters_Summary", + description = + s""" + |# ABAC Rule Parameters Summary + | + |The ABAC Rules Engine provides 18 parameters to your rule function, organized into three categories: + | + |## User Parameters (6 parameters) + | + |1. **authenticatedUser: User** - The logged-in user (always present) + |2. **authenticatedUserAttributes: List[UserAttributeTrait]** - Non-personal attributes of authenticated user + |3. **authenticatedUserAuthContext: List[UserAuthContext]** - Auth context of authenticated user + |4. **onBehalfOfUserOpt: Option[User]** - User being acted on behalf of (delegation) + |5. **onBehalfOfUserAttributes: List[UserAttributeTrait]** - Attributes of delegated user + |6. **onBehalfOfUserAuthContext: List[UserAuthContext]** - Auth context of delegated user + | + |## Target User Parameters (3 parameters) + | + |7. **userOpt: Option[User]** - Target user being evaluated + |8. **userAttributes: List[UserAttributeTrait]** - Attributes of target user + |9. **user: User** - Resolved target user (defaults to authenticatedUser) + | + |## Resource Context Parameters (9 parameters) + | + |10. **bankOpt: Option[Bank]** - Bank context (if bank_id provided) + |11. **bankAttributes: List[BankAttributeTrait]** - Bank attributes + |12. **accountOpt: Option[BankAccount]** - Account context (if account_id provided) + |13. **accountAttributes: List[AccountAttribute]** - Account attributes + |14. **transactionOpt: Option[Transaction]** - Transaction context (if transaction_id provided) + |15. **transactionAttributes: List[TransactionAttribute]** - Transaction attributes + |16. **transactionRequestOpt: Option[TransactionRequest]** - Transaction request context + |17. **transactionRequestAttributes: List[TransactionRequestAttributeTrait]** - Transaction request attributes + |18. **customerOpt: Option[Customer]** - Customer context (if customer_id provided) + |19. **customerAttributes: List[CustomerAttribute]** - Customer attributes + | + |## Usage in Rules + | + |```scala + |// Access user email + |authenticatedUser.emailAddress + | + |// Check if account exists and has sufficient balance + |accountOpt.exists(account => account.balance.toDouble >= 1000.0) + | + |// Check user attributes + |authenticatedUserAttributes.exists(attr => + | attr.name == "role" && attr.value == "admin" + |) + | + |// Check delegation + |onBehalfOfUserOpt.isDefined + |``` + | + |**Related Documentation:** + |- ABAC_Simple_Guide - Getting started guide + |- ABAC_Object_Properties_Reference - Detailed property reference + |""".stripMargin) + + glossaryItems += GlossaryItem( + title = "ABAC_Object_Properties_Reference", + description = + s""" + |# ABAC Object Properties Reference + | + |This document lists all properties available on objects passed to ABAC rules. + | + |## User Object + | + |Available as: `authenticatedUser`, `user`, `onBehalfOfUserOpt.get` + | + |### Core Properties + | + |```scala + |user.userId // String - Unique user ID + |user.emailAddress // String - User's email + |user.name // String - Display name + |user.provider // String - Auth provider + |user.providerId // String - Provider's user ID + |``` + | + |### Usage Examples + | + |```scala + |// Check if user is admin + |user.emailAddress.endsWith("@admin.com") + | + |// Check specific user + |user.userId == "alice@example.com" + |``` + | + |## BankAccount Object + | + |Available as: `accountOpt.get` + | + |### Core Properties + | + |```scala + |account.accountId // AccountId - Account identifier + |account.bankId // BankId - Bank identifier + |account.accountType // String - Account type + |account.balance // BigDecimal - Current balance + |account.currency // String - Currency code (e.g., "EUR") + |account.name // String - Account name + |account.label // String - Account label + |account.owners // List[User] - Account owners + |``` + | + |### Usage Examples + | + |```scala + |// Check balance + |accountOpt.exists(_.balance.toDouble >= 1000.0) + | + |// Check ownership + |accountOpt.exists(account => + | account.owners.exists(owner => owner.userId == user.userId) + |) + | + |// Check currency + |accountOpt.exists(_.currency == "EUR") + |``` + | + |## Bank Object + | + |Available as: `bankOpt.get` + | + |### Core Properties + | + |```scala + |bank.bankId // BankId - Bank identifier + |bank.shortName // String - Short name + |bank.fullName // String - Full legal name + |bank.logoUrl // String - URL to bank logo + |bank.websiteUrl // String - Bank website URL + |bank.bankRoutingScheme // String - Routing scheme + |bank.bankRoutingAddress // String - Routing address + |``` + | + |### Usage Examples + | + |```scala + |// Check specific bank + |bankOpt.exists(_.bankId.value == "gh.29.uk") + | + |// Check bank by routing + |bankOpt.exists(_.bankRoutingScheme == "SWIFT_BIC") + |``` + | + |## Transaction Object + | + |Available as: `transactionOpt.get` + | + |### Core Properties + | + |```scala + |transaction.id // TransactionId - Transaction ID + |transaction.amount // BigDecimal - Transaction amount + |transaction.currency // String - Currency code + |transaction.description // String - Description + |transaction.startDate // Option[Date] - Posted date + |transaction.finishDate // Option[Date] - Completed date + |transaction.transactionType // String - Transaction type + |``` + | + |### Usage Examples + | + |```scala + |// Check transaction amount + |transactionOpt.exists(tx => tx.amount.abs.toDouble < 100.0) + | + |// Check transaction type + |transactionOpt.exists(_.transactionType == "SEPA") + |``` + | + |## Customer Object + | + |Available as: `customerOpt.get` + | + |### Core Properties + | + |```scala + |customer.customerId // String - Customer ID + |customer.customerNumber // String - Customer number + |customer.legalName // String - Legal name + |customer.mobileNumber // String - Mobile number + |customer.email // String - Email address + |customer.dateOfBirth // Date - Date of birth + |``` + | + |### Usage Examples + | + |```scala + |// Check customer email domain + |customerOpt.exists(_.email.endsWith("@company.com")) + |``` + | + |## Attribute Objects + | + |### UserAttributeTrait + | + |```scala + |attr.name // String - Attribute name + |attr.value // String - Attribute value + |attr.attributeType // UserAttributeType - Type of attribute + |``` + | + |### Usage Example + | + |```scala + |// Check for specific attribute + |authenticatedUserAttributes.exists(attr => + | attr.name == "department" && attr.value == "finance" + |) + |``` + | + |**Related Documentation:** + |- ABAC_Simple_Guide - Getting started guide + |- ABAC_Parameters_Summary - Complete parameter list + |""".stripMargin) + + glossaryItems += GlossaryItem( + title = "ABAC_Testing_Examples", + description = + s""" + |# ABAC Testing Examples + | + |## API Endpoint + | + |``` + |POST $getObpApiRoot/v6.0.0/management/abac-rules/{RULE_ID}/execute + |``` + | + |## Example 1: Admin Only Rule + | + |**Rule Code:** + |```scala + |authenticatedUser.emailAddress.endsWith("@admin.com") + |``` + | + |**Test Request:** + |```bash + |curl -X POST \\ + | '$getObpApiRoot/v6.0.0/management/abac-rules/admin-only-rule/execute' \\ + | -H 'Authorization: DirectLogin token=YOUR_TOKEN' \\ + | -H 'Content-Type: application/json' \\ + | -d '{}' + |``` + | + |**Expected Result:** + |- Admin user → `{"result": true}` + |- Regular user → `{"result": false}` + | + |## Example 2: Account Owner Check + | + |**Rule Code:** + |```scala + |accountOpt.exists(account => + | account.owners.exists(owner => owner.userId == user.userId) + |) + |``` + | + |**Test Request:** + |```bash + |curl -X POST \\ + | '$getObpApiRoot/v6.0.0/management/abac-rules/account-owner-only/execute' \\ + | -H 'Authorization: DirectLogin token=YOUR_TOKEN' \\ + | -H 'Content-Type: application/json' \\ + | -d '{ + | "user_id": "alice@example.com", + | "bank_id": "gh.29.uk", + | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" + | }' + |``` + | + |## Example 3: Balance Check + | + |**Rule Code:** + |```scala + |accountOpt.exists(account => account.balance.toDouble >= 1000.0) + |``` + | + |**Test Request:** + |```bash + |curl -X POST \\ + | '$getObpApiRoot/v6.0.0/management/abac-rules/high-balance-only/execute' \\ + | -H 'Authorization: DirectLogin token=YOUR_TOKEN' \\ + | -H 'Content-Type: application/json' \\ + | -d '{ + | "bank_id": "gh.29.uk", + | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0" + | }' + |``` + | + |## Example 4: Transaction Amount Check + | + |**Rule Code:** + |```scala + |transactionOpt.exists(tx => tx.amount.abs.toDouble < 100.0) + |``` + | + |**Test Request:** + |```bash + |curl -X POST \\ + | '$getObpApiRoot/v6.0.0/management/abac-rules/small-transactions/execute' \\ + | -H 'Authorization: DirectLogin token=YOUR_TOKEN' \\ + | -H 'Content-Type: application/json' \\ + | -d '{ + | "bank_id": "gh.29.uk", + | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | "transaction_id": "trans-123" + | }' + |``` + | + |## Testing Patterns + | + |### Pattern 1: Test Different Users + | + |```bash + |# Test for admin + |curl -X POST '$getObpApiRoot/v6.0.0/management/abac-rules/RULE_ID/execute' \\ + | -d '{"user_id": "admin@admin.com", "bank_id": "gh.29.uk"}' + | + |# Test for regular user + |curl -X POST '$getObpApiRoot/v6.0.0/management/abac-rules/RULE_ID/execute' \\ + | -d '{"user_id": "alice@example.com", "bank_id": "gh.29.uk"}' + |``` + | + |### Pattern 2: Test Edge Cases + | + |```bash + |# No context (minimal) + |curl -X POST '$getObpApiRoot/v6.0.0/management/abac-rules/RULE_ID/execute' -d '{}' + | + |# Full context + |curl -X POST '$getObpApiRoot/v6.0.0/management/abac-rules/RULE_ID/execute' -d '{ + | "user_id": "alice@example.com", + | "bank_id": "gh.29.uk", + | "account_id": "8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0", + | "transaction_id": "trans-123", + | "customer_id": "cust-456" + |}' + |``` + | + |## Common Errors + | + |### Error 1: Rule Not Found + | + |```bash + |curl -X POST '$getObpApiRoot/v6.0.0/management/abac-rules/nonexistent-rule/execute' \\ + | -H 'Authorization: DirectLogin token=YOUR_TOKEN' \\ + | -d '{}' + |``` + | + |**Response:** `{"error": "ABAC Rule not found with ID: nonexistent-rule"}` + | + |### Error 2: Invalid Context + | + |**Response:** Objects will be `None` if IDs are invalid, rule should handle gracefully + | + |**Related Documentation:** + |- ABAC_Simple_Guide - Getting started guide + |- ABAC_Parameters_Summary - Complete parameter list + |- ABAC_Object_Properties_Reference - Property reference + |""".stripMargin) + private def getContentFromMarkdownFile(path: String): String = { val source = scala.io.Source.fromFile(path) val lines: String = try source.mkString finally source.close() From 7a5db31972bce8d2ea6891e6b527cd4efb8479a6 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 22:00:22 +0100 Subject: [PATCH 04/19] docfix: removing assumptions in Glossary.scala --- .../main/scala/code/api/util/Glossary.scala | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index 5f11ca078..c9c66147a 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -3937,9 +3937,8 @@ object Glossary extends MdcLoggable { |### 1. `authenticatedUserId` (Required) |**The person actually logged in and making the API call** | - |- This is ALWAYS the real user who authenticated + |- The real user who authenticated |- Retrieved from the authentication token - |- Cannot be faked or changed | |### 2. `onBehalfOfUserId` (Optional) |**When someone acts on behalf of another user (delegation)** @@ -3984,13 +3983,13 @@ object Glossary extends MdcLoggable { |## Available Objects in Rules | |```scala - |authenticatedUser: User // Always present - the logged in user - |onBehalfOfUserOpt: Option[User] // Present if delegation - |user: User // Always present - the target user - |bankOpt: Option[Bank] // Present if bank_id provided - |accountOpt: Option[BankAccount] // Present if account_id provided - |transactionOpt: Option[Transaction] // Present if transaction_id provided - |customerOpt: Option[Customer] // Present if customer_id provided + |authenticatedUser: User // The logged in user + |onBehalfOfUserOpt: Option[User] // User being acted on behalf of (if provided) + |user: User // The target user being evaluated + |bankOpt: Option[Bank] // Bank context (if bank_id provided) + |accountOpt: Option[BankAccount] // Account context (if account_id provided) + |transactionOpt: Option[Transaction] // Transaction context (if transaction_id provided) + |customerOpt: Option[Customer] // Customer context (if customer_id provided) |``` | |**Related Documentation:** @@ -4009,17 +4008,17 @@ object Glossary extends MdcLoggable { | |## User Parameters (6 parameters) | - |1. **authenticatedUser: User** - The logged-in user (always present) - |2. **authenticatedUserAttributes: List[UserAttributeTrait]** - Non-personal attributes of authenticated user + |1. **authenticatedUser: User** - The logged-in user + |2. **authenticatedUserAttributes: List[UserAttributeTrait]** - Non-personal attributes of authenticated user (IsPersonal=false) |3. **authenticatedUserAuthContext: List[UserAuthContext]** - Auth context of authenticated user - |4. **onBehalfOfUserOpt: Option[User]** - User being acted on behalf of (delegation) - |5. **onBehalfOfUserAttributes: List[UserAttributeTrait]** - Attributes of delegated user - |6. **onBehalfOfUserAuthContext: List[UserAuthContext]** - Auth context of delegated user + |4. **onBehalfOfUserOpt: Option[User]** - User being acted on behalf of (if provided) + |5. **onBehalfOfUserAttributes: List[UserAttributeTrait]** - Non-personal attributes of on-behalf-of user (IsPersonal=false) + |6. **onBehalfOfUserAuthContext: List[UserAuthContext]** - Auth context of on-behalf-of user | |## Target User Parameters (3 parameters) | |7. **userOpt: Option[User]** - Target user being evaluated - |8. **userAttributes: List[UserAttributeTrait]** - Attributes of target user + |8. **userAttributes: List[UserAttributeTrait]** - Non-personal attributes of target user (IsPersonal=false) |9. **user: User** - Resolved target user (defaults to authenticatedUser) | |## Resource Context Parameters (9 parameters) @@ -4044,11 +4043,13 @@ object Glossary extends MdcLoggable { |// Check if account exists and has sufficient balance |accountOpt.exists(account => account.balance.toDouble >= 1000.0) | - |// Check user attributes + |// Check user attributes (non-personal only) |authenticatedUserAttributes.exists(attr => | attr.name == "role" && attr.value == "admin" |) | + |// Note: Only non-personal attributes (IsPersonal=false) are included + | |// Check delegation |onBehalfOfUserOpt.isDefined |``` @@ -4209,10 +4210,14 @@ object Glossary extends MdcLoggable { |### Usage Example | |```scala - |// Check for specific attribute + |// Check for specific non-personal attribute |authenticatedUserAttributes.exists(attr => | attr.name == "department" && attr.value == "finance" |) + | + |// Note: User attributes in ABAC rules only include non-personal attributes + |// (where IsPersonal=false). Personal attributes are not available for + |// privacy and GDPR compliance reasons. |``` | |**Related Documentation:** From cc05c56a299665d972818c28384d1408a293773b Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 22:44:38 +0100 Subject: [PATCH 05/19] v6.0.0 users/USER_ID/attributes and /my/personal-data endpoints to make user attributes more like other attributes --- .../main/scala/code/api/util/ApiRole.scala | 13 + .../scala/code/api/v6_0_0/APIMethods600.scala | 567 ++++++++++++++++++ 2 files changed, 580 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index f50c13b48..ea5ee7592 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -480,6 +480,19 @@ object ApiRole extends MdcLoggable{ case class CanDeleteNonPersonalUserAttribute (requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteNonPersonalUserAttribute = CanDeleteNonPersonalUserAttribute() + // v6.0.0 User Attribute roles (consistent naming - "user attributes" means non-personal) + case class CanCreateUserAttribute (requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateUserAttribute = CanCreateUserAttribute() + + case class CanGetUserAttributes (requiresBankId: Boolean = false) extends ApiRole + lazy val canGetUserAttributes = CanGetUserAttributes() + + case class CanUpdateUserAttribute (requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateUserAttribute = CanUpdateUserAttribute() + + case class CanDeleteUserAttribute (requiresBankId: Boolean = false) extends ApiRole + lazy val canDeleteUserAttribute = CanDeleteUserAttribute() + case class CanReadUserLockedStatus(requiresBankId: Boolean = false) extends ApiRole lazy val canReadUserLockedStatus = CanReadUserLockedStatus() diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 3931ad03a..192ec71d1 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4569,6 +4569,573 @@ trait APIMethods600 { } } + // ============================================================================================================ + // USER ATTRIBUTES v6.0.0 - Consistent with other entity attributes + // ============================================================================================================ + // "user attributes" = IsPersonal=false (requires roles) - consistent with other entity attributes + // "personal user attributes" = IsPersonal=true (no roles, user manages their own) + // ============================================================================================================ + + staticResourceDocs += ResourceDoc( + createUserAttribute, + implementedInApiVersion, + nameOf(createUserAttribute), + "POST", + "/users/USER_ID/attributes", + "Create User Attribute", + s"""Create a User Attribute for the user specified by USER_ID. + | + |User Attributes are non-personal attributes (IsPersonal=false) that can be used in ABAC rules. + |They require a role to set, similar to Customer Attributes, Account Attributes, etc. + | + |For personal attributes that users manage themselves, see the /my/personal-user-attributes endpoints. + | + |The type field must be one of "STRING", "INTEGER", "DOUBLE" or "DATE_WITH_DAY" + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + code.api.v5_1_0.UserAttributeJsonV510( + name = "account_type", + `type` = "STRING", + value = "premium" + ), + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "account_type", + `type` = "STRING", + value = "premium", + is_personal = false, + insert_date = exampleDate + ), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UserNotFoundByUserId, + InvalidJsonFormat, + UnknownError + ), + List(apiTagUser), + Some(List(canCreateUserAttribute)) + ) + + lazy val createUserAttribute: OBPEndpoint = { + case "users" :: userId :: "attributes" :: Nil JsonPost json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateUserAttribute, callContext) + (user, callContext) <- NewStyle.function.getUserByUserId(userId, callContext) + failMsg = s"$InvalidJsonFormat The Json body should be the UserAttributeJsonV510" + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[code.api.v5_1_0.UserAttributeJsonV510] + } + failMsg = s"$InvalidJsonFormat The `type` field can only accept: ${UserAttributeType.DOUBLE}, ${UserAttributeType.STRING}, ${UserAttributeType.INTEGER}, ${UserAttributeType.DATE_WITH_DAY}" + userAttributeType <- NewStyle.function.tryons(failMsg, 400, callContext) { + UserAttributeType.withName(postedData.`type`) + } + (userAttribute, callContext) <- NewStyle.function.createOrUpdateUserAttribute( + user.userId, + None, + postedData.name, + userAttributeType, + postedData.value, + false, // IsPersonal = false for user attributes + callContext + ) + } yield { + (JSONFactory510.createUserAttributeJson(userAttribute), HttpCode.`201`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getUserAttributes, + implementedInApiVersion, + nameOf(getUserAttributes), + "GET", + "/users/USER_ID/attributes", + "Get User Attributes", + s"""Get User Attributes for the user specified by USER_ID. + | + |Returns non-personal user attributes (IsPersonal=false) that can be used in ABAC rules. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + code.api.v5_1_0.UserAttributesResponseJsonV510( + user_attributes = List( + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "account_type", + `type` = "STRING", + value = "premium", + is_personal = false, + insert_date = exampleDate + ) + ) + ), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UserNotFoundByUserId, + UnknownError + ), + List(apiTagUser), + Some(List(canGetUserAttributes)) + ) + + lazy val getUserAttributes: OBPEndpoint = { + case "users" :: userId :: "attributes" :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canGetUserAttributes, callContext) + (user, callContext) <- NewStyle.function.getUserByUserId(userId, callContext) + (attributes, callContext) <- NewStyle.function.getNonPersonalUserAttributes(user.userId, callContext) + } yield { + (code.api.v5_1_0.UserAttributesResponseJsonV510(attributes.map(JSONFactory510.createUserAttributeJson)), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getUserAttributeById, + implementedInApiVersion, + nameOf(getUserAttributeById), + "GET", + "/users/USER_ID/attributes/USER_ATTRIBUTE_ID", + "Get User Attribute By Id", + s"""Get a User Attribute by USER_ATTRIBUTE_ID for the user specified by USER_ID. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "account_type", + `type` = "STRING", + value = "premium", + is_personal = false, + insert_date = exampleDate + ), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UserNotFoundByUserId, + UserAttributeNotFound, + UnknownError + ), + List(apiTagUser), + Some(List(canGetUserAttributes)) + ) + + lazy val getUserAttributeById: OBPEndpoint = { + case "users" :: userId :: "attributes" :: userAttributeId :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canGetUserAttributes, callContext) + (user, callContext) <- NewStyle.function.getUserByUserId(userId, callContext) + (attributes, callContext) <- NewStyle.function.getNonPersonalUserAttributes(user.userId, callContext) + attribute <- Future { + attributes.find(_.userAttributeId == userAttributeId) + } map { + unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) + } + } yield { + (JSONFactory510.createUserAttributeJson(attribute), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + updateUserAttribute, + implementedInApiVersion, + nameOf(updateUserAttribute), + "PUT", + "/users/USER_ID/attributes/USER_ATTRIBUTE_ID", + "Update User Attribute", + s"""Update a User Attribute by USER_ATTRIBUTE_ID for the user specified by USER_ID. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + code.api.v5_1_0.UserAttributeJsonV510( + name = "account_type", + `type` = "STRING", + value = "enterprise" + ), + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "account_type", + `type` = "STRING", + value = "enterprise", + is_personal = false, + insert_date = exampleDate + ), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UserNotFoundByUserId, + UserAttributeNotFound, + InvalidJsonFormat, + UnknownError + ), + List(apiTagUser), + Some(List(canUpdateUserAttribute)) + ) + + lazy val updateUserAttribute: OBPEndpoint = { + case "users" :: userId :: "attributes" :: userAttributeId :: Nil JsonPut json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canUpdateUserAttribute, callContext) + (user, callContext) <- NewStyle.function.getUserByUserId(userId, callContext) + failMsg = s"$InvalidJsonFormat The Json body should be the UserAttributeJsonV510" + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[code.api.v5_1_0.UserAttributeJsonV510] + } + failMsg = s"$InvalidJsonFormat The `type` field can only accept: ${UserAttributeType.DOUBLE}, ${UserAttributeType.STRING}, ${UserAttributeType.INTEGER}, ${UserAttributeType.DATE_WITH_DAY}" + userAttributeType <- NewStyle.function.tryons(failMsg, 400, callContext) { + UserAttributeType.withName(postedData.`type`) + } + (attributes, callContext) <- NewStyle.function.getNonPersonalUserAttributes(user.userId, callContext) + _ <- Future { + attributes.find(_.userAttributeId == userAttributeId) + } map { + unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) + } + (userAttribute, callContext) <- NewStyle.function.createOrUpdateUserAttribute( + user.userId, + Some(userAttributeId), + postedData.name, + userAttributeType, + postedData.value, + false, // IsPersonal = false for user attributes + callContext + ) + } yield { + (JSONFactory510.createUserAttributeJson(userAttribute), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteUserAttribute, + implementedInApiVersion, + nameOf(deleteUserAttribute), + "DELETE", + "/users/USER_ID/attributes/USER_ATTRIBUTE_ID", + "Delete User Attribute", + s"""Delete a User Attribute by USER_ATTRIBUTE_ID for the user specified by USER_ID. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + EmptyBody, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UserNotFoundByUserId, + UserAttributeNotFound, + UnknownError + ), + List(apiTagUser), + Some(List(canDeleteUserAttribute)) + ) + + lazy val deleteUserAttribute: OBPEndpoint = { + case "users" :: userId :: "attributes" :: userAttributeId :: Nil JsonDelete _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteUserAttribute, callContext) + (user, callContext) <- NewStyle.function.getUserByUserId(userId, callContext) + (attributes, callContext) <- NewStyle.function.getNonPersonalUserAttributes(user.userId, callContext) + _ <- Future { + attributes.find(_.userAttributeId == userAttributeId) + } map { + unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) + } + (deleted, callContext) <- NewStyle.function.deleteUserAttribute(userAttributeId, callContext) + } yield { + (Full(deleted), HttpCode.`204`(callContext)) + } + } + } + + // ============================================================================================================ + // PERSONAL DATA - User manages their own personal data + // ============================================================================================================ + + staticResourceDocs += ResourceDoc( + createMyPersonalUserAttribute, + implementedInApiVersion, + nameOf(createMyPersonalUserAttribute), + "POST", + "/my/personal-data", + "Create My Personal Data", + s"""Create Personal Data for the currently authenticated user. + | + |Personal Data (IsPersonal=true) is managed by the user themselves and does not require special roles. + |This data is not available in ABAC rules for privacy reasons. + | + |For non-personal attributes that can be used in ABAC rules, see the /users/USER_ID/attributes endpoints. + | + |The type field must be one of "STRING", "INTEGER", "DOUBLE" or "DATE_WITH_DAY" + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + code.api.v5_1_0.UserAttributeJsonV510( + name = "favorite_color", + `type` = "STRING", + value = "blue" + ), + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "favorite_color", + `type` = "STRING", + value = "blue", + is_personal = true, + insert_date = exampleDate + ), + List( + $UserNotLoggedIn, + InvalidJsonFormat, + UnknownError + ), + List(apiTagUser), + Some(List()) + ) + + lazy val createMyPersonalUserAttribute: OBPEndpoint = { + case "my" :: "personal-data" :: Nil JsonPost json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + failMsg = s"$InvalidJsonFormat The Json body should be the UserAttributeJsonV510" + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[code.api.v5_1_0.UserAttributeJsonV510] + } + failMsg = s"$InvalidJsonFormat The `type` field can only accept: ${UserAttributeType.DOUBLE}, ${UserAttributeType.STRING}, ${UserAttributeType.INTEGER}, ${UserAttributeType.DATE_WITH_DAY}" + userAttributeType <- NewStyle.function.tryons(failMsg, 400, callContext) { + UserAttributeType.withName(postedData.`type`) + } + (userAttribute, callContext) <- NewStyle.function.createOrUpdateUserAttribute( + u.userId, + None, + postedData.name, + userAttributeType, + postedData.value, + true, // IsPersonal = true for personal user attributes + callContext + ) + } yield { + (JSONFactory510.createUserAttributeJson(userAttribute), HttpCode.`201`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getMyPersonalUserAttributes, + implementedInApiVersion, + nameOf(getMyPersonalUserAttributes), + "GET", + "/my/personal-data", + "Get My Personal Data", + s"""Get Personal Data for the currently authenticated user. + | + |Returns personal data (IsPersonal=true) that is managed by the user. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + code.api.v5_1_0.UserAttributesResponseJsonV510( + user_attributes = List( + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "favorite_color", + `type` = "STRING", + value = "blue", + is_personal = true, + insert_date = exampleDate + ) + ) + ), + List( + $UserNotLoggedIn, + UnknownError + ), + List(apiTagUser), + Some(List()) + ) + + lazy val getMyPersonalUserAttributes: OBPEndpoint = { + case "my" :: "personal-data" :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + (attributes, callContext) <- NewStyle.function.getPersonalUserAttributes(u.userId, callContext) + } yield { + (code.api.v5_1_0.UserAttributesResponseJsonV510(attributes.map(JSONFactory510.createUserAttributeJson)), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getMyPersonalUserAttributeById, + implementedInApiVersion, + nameOf(getMyPersonalUserAttributeById), + "GET", + "/my/personal-data/USER_ATTRIBUTE_ID", + "Get My Personal Data By Id", + s"""Get Personal Data by USER_ATTRIBUTE_ID for the currently authenticated user. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "favorite_color", + `type` = "STRING", + value = "blue", + is_personal = true, + insert_date = exampleDate + ), + List( + $UserNotLoggedIn, + UserAttributeNotFound, + UnknownError + ), + List(apiTagUser), + Some(List()) + ) + + lazy val getMyPersonalUserAttributeById: OBPEndpoint = { + case "my" :: "personal-data" :: userAttributeId :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + (attributes, callContext) <- NewStyle.function.getPersonalUserAttributes(u.userId, callContext) + attribute <- Future { + attributes.find(_.userAttributeId == userAttributeId) + } map { + unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) + } + } yield { + (JSONFactory510.createUserAttributeJson(attribute), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + updateMyPersonalUserAttribute, + implementedInApiVersion, + nameOf(updateMyPersonalUserAttribute), + "PUT", + "/my/personal-data/USER_ATTRIBUTE_ID", + "Update My Personal Data", + s"""Update Personal Data by USER_ATTRIBUTE_ID for the currently authenticated user. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + code.api.v5_1_0.UserAttributeJsonV510( + name = "favorite_color", + `type` = "STRING", + value = "green" + ), + code.api.v5_1_0.UserAttributeResponseJsonV510( + user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", + name = "favorite_color", + `type` = "STRING", + value = "green", + is_personal = true, + insert_date = exampleDate + ), + List( + $UserNotLoggedIn, + UserAttributeNotFound, + InvalidJsonFormat, + UnknownError + ), + List(apiTagUser), + Some(List()) + ) + + lazy val updateMyPersonalUserAttribute: OBPEndpoint = { + case "my" :: "personal-data" :: userAttributeId :: Nil JsonPut json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + failMsg = s"$InvalidJsonFormat The Json body should be the UserAttributeJsonV510" + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + json.extract[code.api.v5_1_0.UserAttributeJsonV510] + } + failMsg = s"$InvalidJsonFormat The `type` field can only accept: ${UserAttributeType.DOUBLE}, ${UserAttributeType.STRING}, ${UserAttributeType.INTEGER}, ${UserAttributeType.DATE_WITH_DAY}" + userAttributeType <- NewStyle.function.tryons(failMsg, 400, callContext) { + UserAttributeType.withName(postedData.`type`) + } + (attributes, callContext) <- NewStyle.function.getPersonalUserAttributes(u.userId, callContext) + _ <- Future { + attributes.find(_.userAttributeId == userAttributeId) + } map { + unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) + } + (userAttribute, callContext) <- NewStyle.function.createOrUpdateUserAttribute( + u.userId, + Some(userAttributeId), + postedData.name, + userAttributeType, + postedData.value, + true, // IsPersonal = true for personal user attributes + callContext + ) + } yield { + (JSONFactory510.createUserAttributeJson(userAttribute), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteMyPersonalUserAttribute, + implementedInApiVersion, + nameOf(deleteMyPersonalUserAttribute), + "DELETE", + "/my/personal-data/USER_ATTRIBUTE_ID", + "Delete My Personal Data", + s"""Delete Personal Data by USER_ATTRIBUTE_ID for the currently authenticated user. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + EmptyBody, + List( + $UserNotLoggedIn, + UserAttributeNotFound, + UnknownError + ), + List(apiTagUser), + Some(List()) + ) + + lazy val deleteMyPersonalUserAttribute: OBPEndpoint = { + case "my" :: "personal-data" :: userAttributeId :: Nil JsonDelete _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + (attributes, callContext) <- NewStyle.function.getPersonalUserAttributes(u.userId, callContext) + _ <- Future { + attributes.find(_.userAttributeId == userAttributeId) + } map { + unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) + } + (deleted, callContext) <- NewStyle.function.deleteUserAttribute(userAttributeId, callContext) + } yield { + (Full(deleted), HttpCode.`204`(callContext)) + } + } + } + } } From 6213d0e9fbcefb772efba0b0c166dcbf128013ca Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 22:54:35 +0100 Subject: [PATCH 06/19] v6.0.0 users/USER_ID/attributes and /my/personal-data endpoints to make user attributes more like other attributes 2 --- .../scala/code/api/v6_0_0/APIMethods600.scala | 113 +++++------------- 1 file changed, 32 insertions(+), 81 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 192ec71d1..dd396ddc7 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -31,7 +31,7 @@ import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV import code.api.v6_0_0.OBPAPI6_0_0 import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider} import code.metrics.APIMetrics -import code.bankconnectors.LocalMappedConnectorInternal +import code.bankconnectors.{Connector, LocalMappedConnectorInternal} import code.bankconnectors.LocalMappedConnectorInternal._ import code.entitlement.Entitlement import code.loginattempts.LoginAttempt @@ -49,6 +49,7 @@ import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.{CustomerAttribute, _} import com.openbankproject.commons.model.enums.DynamicEntityOperation._ +import com.openbankproject.commons.model.enums.UserAttributeType import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import net.liftweb.common.{Empty, Failure, Full} import org.apache.commons.lang3.StringUtils @@ -4592,21 +4593,14 @@ trait APIMethods600 { | |The type field must be one of "STRING", "INTEGER", "DOUBLE" or "DATE_WITH_DAY" | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, code.api.v5_1_0.UserAttributeJsonV510( name = "account_type", `type` = "STRING", value = "premium" ), - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "account_type", - `type` = "STRING", - value = "premium", - is_personal = false, - insert_date = exampleDate - ), + userAttributeResponseJsonV510, List( $UserNotLoggedIn, UserHasMissingRoles, @@ -4659,20 +4653,11 @@ trait APIMethods600 { | |Returns non-personal user attributes (IsPersonal=false) that can be used in ABAC rules. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, EmptyBody, code.api.v5_1_0.UserAttributesResponseJsonV510( - user_attributes = List( - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "account_type", - `type` = "STRING", - value = "premium", - is_personal = false, - insert_date = exampleDate - ) - ) + user_attributes = List(userAttributeResponseJsonV510) ), List( $UserNotLoggedIn, @@ -4707,17 +4692,10 @@ trait APIMethods600 { "Get User Attribute By Id", s"""Get a User Attribute by USER_ATTRIBUTE_ID for the user specified by USER_ID. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, EmptyBody, - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "account_type", - `type` = "STRING", - value = "premium", - is_personal = false, - insert_date = exampleDate - ), + userAttributeResponseJsonV510, List( $UserNotLoggedIn, UserHasMissingRoles, @@ -4757,21 +4735,14 @@ trait APIMethods600 { "Update User Attribute", s"""Update a User Attribute by USER_ATTRIBUTE_ID for the user specified by USER_ID. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, code.api.v5_1_0.UserAttributeJsonV510( name = "account_type", `type` = "STRING", value = "enterprise" ), - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "account_type", - `type` = "STRING", - value = "enterprise", - is_personal = false, - insert_date = exampleDate - ), + userAttributeResponseJsonV510, List( $UserNotLoggedIn, UserHasMissingRoles, @@ -4829,7 +4800,7 @@ trait APIMethods600 { "Delete User Attribute", s"""Delete a User Attribute by USER_ATTRIBUTE_ID for the user specified by USER_ID. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, EmptyBody, EmptyBody, @@ -4857,7 +4828,12 @@ trait APIMethods600 { } map { unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) } - (deleted, callContext) <- NewStyle.function.deleteUserAttribute(userAttributeId, callContext) + (deleted, callContext) <- Connector.connector.vend.deleteUserAttribute( + userAttributeId, + callContext + ) map { + i => (connectorEmptyResponse(i._1, callContext), i._2) + } } yield { (Full(deleted), HttpCode.`204`(callContext)) } @@ -4884,21 +4860,14 @@ trait APIMethods600 { | |The type field must be one of "STRING", "INTEGER", "DOUBLE" or "DATE_WITH_DAY" | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, code.api.v5_1_0.UserAttributeJsonV510( name = "favorite_color", `type` = "STRING", value = "blue" ), - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "favorite_color", - `type` = "STRING", - value = "blue", - is_personal = true, - insert_date = exampleDate - ), + userAttributeResponseJsonV510, List( $UserNotLoggedIn, InvalidJsonFormat, @@ -4947,20 +4916,11 @@ trait APIMethods600 { | |Returns personal data (IsPersonal=true) that is managed by the user. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, EmptyBody, code.api.v5_1_0.UserAttributesResponseJsonV510( - user_attributes = List( - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "favorite_color", - `type` = "STRING", - value = "blue", - is_personal = true, - insert_date = exampleDate - ) - ) + user_attributes = List(userAttributeResponseJsonV510) ), List( $UserNotLoggedIn, @@ -4991,17 +4951,10 @@ trait APIMethods600 { "Get My Personal Data By Id", s"""Get Personal Data by USER_ATTRIBUTE_ID for the currently authenticated user. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, EmptyBody, - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "favorite_color", - `type` = "STRING", - value = "blue", - is_personal = true, - insert_date = exampleDate - ), + userAttributeResponseJsonV510, List( $UserNotLoggedIn, UserAttributeNotFound, @@ -5037,21 +4990,14 @@ trait APIMethods600 { "Update My Personal Data", s"""Update Personal Data by USER_ATTRIBUTE_ID for the currently authenticated user. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, code.api.v5_1_0.UserAttributeJsonV510( name = "favorite_color", `type` = "STRING", value = "green" ), - code.api.v5_1_0.UserAttributeResponseJsonV510( - user_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f", - name = "favorite_color", - `type` = "STRING", - value = "green", - is_personal = true, - insert_date = exampleDate - ), + userAttributeResponseJsonV510, List( $UserNotLoggedIn, UserAttributeNotFound, @@ -5105,7 +5051,7 @@ trait APIMethods600 { "Delete My Personal Data", s"""Delete Personal Data by USER_ATTRIBUTE_ID for the currently authenticated user. | - |${authenticationRequiredMessage(true)} + |${userAuthenticationMessage(true)} |""".stripMargin, EmptyBody, EmptyBody, @@ -5129,7 +5075,12 @@ trait APIMethods600 { } map { unboxFullOrFail(_, callContext, UserAttributeNotFound, 404) } - (deleted, callContext) <- NewStyle.function.deleteUserAttribute(userAttributeId, callContext) + (deleted, callContext) <- Connector.connector.vend.deleteUserAttribute( + userAttributeId, + callContext + ) map { + i => (connectorEmptyResponse(i._1, callContext), i._2) + } } yield { (Full(deleted), HttpCode.`204`(callContext)) } From 9be964c88664ca1549bb192069a8866822c8ce48 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:03:19 +0100 Subject: [PATCH 07/19] docfix: apiTagUserAttribute and apiTagAttribute --- .../src/main/scala/code/api/util/ApiTag.scala | 4 +++- .../scala/code/api/v6_0_0/APIMethods600.scala | 20 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 864efed1a..f1d4cdb92 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -45,7 +45,9 @@ object ApiTag { val apiTagCustomer = ResourceDocTag("Customer") val apiTagOnboarding = ResourceDocTag("Onboarding") val apiTagUser = ResourceDocTag("User") // Use for User Management / Info APIs - val apiTagUserInvitation = ResourceDocTag("User-Invitation") + val apiTagUserInvitation = ResourceDocTag("User-Invitation") + val apiTagAttribute = ResourceDocTag("Attribute") + val apiTagUserAttribute = ResourceDocTag("User-Attribute") val apiTagMeeting = ResourceDocTag("Customer-Meeting") val apiTagExperimental = ResourceDocTag("Experimental") val apiTagPerson = ResourceDocTag("Person") diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index dd396ddc7..5485d87d5 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4608,7 +4608,7 @@ trait APIMethods600 { InvalidJsonFormat, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List(canCreateUserAttribute)) ) @@ -4665,7 +4665,7 @@ trait APIMethods600 { UserNotFoundByUserId, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List(canGetUserAttributes)) ) @@ -4703,7 +4703,7 @@ trait APIMethods600 { UserAttributeNotFound, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List(canGetUserAttributes)) ) @@ -4751,7 +4751,7 @@ trait APIMethods600 { InvalidJsonFormat, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List(canUpdateUserAttribute)) ) @@ -4811,7 +4811,7 @@ trait APIMethods600 { UserAttributeNotFound, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List(canDeleteUserAttribute)) ) @@ -4873,7 +4873,7 @@ trait APIMethods600 { InvalidJsonFormat, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List()) ) @@ -4926,7 +4926,7 @@ trait APIMethods600 { $UserNotLoggedIn, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List()) ) @@ -4960,7 +4960,7 @@ trait APIMethods600 { UserAttributeNotFound, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List()) ) @@ -5004,7 +5004,7 @@ trait APIMethods600 { InvalidJsonFormat, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List()) ) @@ -5060,7 +5060,7 @@ trait APIMethods600 { UserAttributeNotFound, UnknownError ), - List(apiTagUser), + List(apiTagUser, apiTagUserAttribute, apiTagAttribute), Some(List()) ) From 6109020328812f7caecbef4e21fc260f4b59cab3 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:07:07 +0100 Subject: [PATCH 08/19] docfix: apiTagCustomerAttribute and use of apiTagAttribute --- .../src/main/scala/code/api/util/ApiTag.scala | 1 + .../scala/code/api/v4_0_0/APIMethods400.scala | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index f1d4cdb92..939cc0c07 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -43,6 +43,7 @@ object ApiTag { val apiTagCounterparty = ResourceDocTag("Counterparty") val apiTagKyc = ResourceDocTag("KYC") val apiTagCustomer = ResourceDocTag("Customer") + val apiTagCustomerAttribute = ResourceDocTag("Customer-Attribute") val apiTagOnboarding = ResourceDocTag("Onboarding") val apiTagUser = ResourceDocTag("User") // Use for User Management / Info APIs val apiTagUserInvitation = ResourceDocTag("User-Invitation") diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 83b9c45e3..074f4a11e 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -6106,7 +6106,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some( List( canCreateCustomerAttributeAtOneBank, @@ -6186,7 +6186,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some( List( canUpdateCustomerAttributeAtOneBank, @@ -6270,7 +6270,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some( List( canGetCustomerAttributesAtOneBank, @@ -6327,7 +6327,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some( List(canGetCustomerAttributeAtOneBank, canGetCustomerAttributeAtAnyBank) ) @@ -7346,7 +7346,7 @@ trait APIMethods400 extends MdcLoggable { UserHasMissingRoles, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some( List( canDeleteCustomerAttributeAtOneBank, @@ -7908,7 +7908,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some(List(canCreateCustomerAttributeDefinitionAtOneBank)) ) @@ -9202,7 +9202,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some(List(canDeleteCustomerAttributeDefinitionAtOneBank)) ) @@ -9408,7 +9408,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagCustomer), + List(apiTagCustomer, apiTagCustomerAttribute, apiTagAttribute), Some(List(canGetCustomerAttributeDefinitionAtOneBank)) ) From 7d6aa4e9c7e2bdd82ae2fc17b180323bfe1498bc Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:09:57 +0100 Subject: [PATCH 09/19] docfix: apiTagAtmAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + .../src/main/scala/code/api/v5_1_0/APIMethods510.scala | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 939cc0c07..c2ad3487e 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -56,6 +56,7 @@ object ApiTag { val apiTagSandbox = ResourceDocTag("Sandbox") val apiTagBranch = ResourceDocTag("Branch") val apiTagATM = ResourceDocTag("ATM") + val apiTagAtmAttribute = ResourceDocTag("ATM-Attribute") val apiTagProduct = ResourceDocTag("Product") val apiTagProductCollection = ResourceDocTag("Product-Collection") val apiTagOpenData = ResourceDocTag("Open-Data") diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index f5ff01e40..6b5c0e479 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1181,7 +1181,7 @@ trait APIMethods510 { InvalidJsonFormat, UnknownError ), - List(apiTagATM), + List(apiTagATM, apiTagAtmAttribute, apiTagAttribute), Some(List(canCreateAtmAttribute, canCreateAtmAttributeAtAnyBank)) ) @@ -1269,7 +1269,7 @@ trait APIMethods510 { InvalidJsonFormat, UnknownError ), - List(apiTagATM), + List(apiTagATM, apiTagAtmAttribute, apiTagAttribute), Some(List(canGetAtmAttribute, canGetAtmAttributeAtAnyBank)) ) @@ -1305,7 +1305,7 @@ trait APIMethods510 { InvalidJsonFormat, UnknownError ), - List(apiTagATM), + List(apiTagATM, apiTagAtmAttribute, apiTagAttribute), Some(List(canGetAtmAttribute, canGetAtmAttributeAtAnyBank)) ) @@ -1344,7 +1344,7 @@ trait APIMethods510 { UserHasMissingRoles, UnknownError ), - List(apiTagATM), + List(apiTagATM, apiTagAtmAttribute, apiTagAttribute), Some(List(canUpdateAtmAttribute, canUpdateAtmAttributeAtAnyBank)) ) @@ -1402,7 +1402,7 @@ trait APIMethods510 { UserHasMissingRoles, UnknownError ), - List(apiTagATM), + List(apiTagATM, apiTagAtmAttribute, apiTagAttribute), Some(List(canDeleteAtmAttribute, canDeleteAtmAttributeAtAnyBank)) ) From af18aaaeb7a9eb3eff4698530e4bd07f784d39e5 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:12:39 +0100 Subject: [PATCH 10/19] docfix: apiTagTransactionAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + .../main/scala/code/api/v4_0_0/APIMethods400.scala | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index c2ad3487e..607c3481b 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -30,6 +30,7 @@ object ApiTag { val apiTagPublicData = ResourceDocTag("PublicData") val apiTagPrivateData = ResourceDocTag("PrivateData") val apiTagTransaction = ResourceDocTag("Transaction") + val apiTagTransactionAttribute = ResourceDocTag("Transaction-Attribute") val apiTagTransactionFirehose = ResourceDocTag("Transaction-Firehose") val apiTagCounterpartyMetaData = ResourceDocTag("Counterparty-Metadata") val apiTagTransactionMetaData = ResourceDocTag("Transaction-Metadata") diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 074f4a11e..0ff1ba56f 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -6446,7 +6446,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canCreateTransactionAttributeAtOneBank)) ) @@ -6519,7 +6519,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canUpdateTransactionAttributeAtOneBank)) ) @@ -6598,7 +6598,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canGetTransactionAttributesAtOneBank)) ) @@ -6652,7 +6652,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canGetTransactionAttributeAtOneBank)) ) @@ -9004,7 +9004,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canCreateTransactionAttributeDefinitionAtOneBank)) ) @@ -9159,7 +9159,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canDeleteTransactionAttributeDefinitionAtOneBank)) ) @@ -9492,7 +9492,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagTransaction), + List(apiTagTransaction, apiTagTransactionAttribute, apiTagAttribute), Some(List(canGetTransactionAttributeDefinitionAtOneBank)) ) From 83671edac74942fa440484207a11a17a3ca4c9a0 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:14:32 +0100 Subject: [PATCH 11/19] docfix: apiTagTransactionRequestAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + .../main/scala/code/api/v4_0_0/APIMethods400.scala | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 607c3481b..1c166ab42 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -15,6 +15,7 @@ object ApiTag { // When using these tags in resource docs, as we now have many APIs, it's best not to have too use too many tags per endpoint. val apiTagOldStyle = ResourceDocTag("Old-Style") val apiTagTransactionRequest = ResourceDocTag("Transaction-Request") + val apiTagTransactionRequestAttribute = ResourceDocTag("Transaction-Request-Attribute") val apiTagVrp = ResourceDocTag("VRP") val apiTagApi = ResourceDocTag("API") val apiTagBank = ResourceDocTag("Bank") diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 0ff1ba56f..7ff51638e 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -1672,7 +1672,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canCreateTransactionRequestAttributeAtOneBank)) ) @@ -1744,7 +1744,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canGetTransactionRequestAttributeAtOneBank)) ) @@ -1798,7 +1798,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canGetTransactionRequestAttributesAtOneBank)) ) @@ -1852,7 +1852,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canUpdateTransactionRequestAttributeAtOneBank)) ) @@ -1935,7 +1935,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canCreateTransactionRequestAttributeDefinitionAtOneBank)) ) @@ -2011,7 +2011,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canGetTransactionRequestAttributeDefinitionAtOneBank)) ) @@ -2058,7 +2058,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagTransactionRequest), + List(apiTagTransactionRequest, apiTagTransactionRequestAttribute, apiTagAttribute), Some(List(canDeleteTransactionRequestAttributeDefinitionAtOneBank)) ) From 47199883183558ad8aceb630949474b22e1d26f3 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:16:29 +0100 Subject: [PATCH 12/19] docfix: apiTagBankAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + .../main/scala/code/api/v4_0_0/APIMethods400.scala | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 1c166ab42..72ca399f5 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -19,6 +19,7 @@ object ApiTag { val apiTagVrp = ResourceDocTag("VRP") val apiTagApi = ResourceDocTag("API") val apiTagBank = ResourceDocTag("Bank") + val apiTagBankAttribute = ResourceDocTag("Bank-Attribute") val apiTagAccount = ResourceDocTag("Account") val apiTagAccountAccess = ResourceDocTag("Account-Access") val apiTagDirectDebit = ResourceDocTag("Direct-Debit") diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 7ff51638e..4ca414ba6 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -8645,7 +8645,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagBank), + List(apiTagBank, apiTagBankAttribute, apiTagAttribute), Some(List(canCreateBankAttributeDefinitionAtOneBank)) ) @@ -8737,7 +8737,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagBank), + List(apiTagBank, apiTagBankAttribute, apiTagAttribute), Some(List(canCreateBankAttribute)) ) @@ -8800,7 +8800,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagBank), + List(apiTagBank, apiTagBankAttribute, apiTagAttribute), Some(List(canGetBankAttribute)) ) @@ -8836,7 +8836,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagBank), + List(apiTagBank, apiTagBankAttribute, apiTagAttribute), Some(List(canGetBankAttribute)) ) @@ -8876,7 +8876,7 @@ trait APIMethods400 extends MdcLoggable { UserHasMissingRoles, UnknownError ), - List(apiTagBank) + List(apiTagBank, apiTagBankAttribute, apiTagAttribute) ) lazy val updateBankAttribute: OBPEndpoint = { @@ -8953,7 +8953,7 @@ trait APIMethods400 extends MdcLoggable { BankNotFound, UnknownError ), - List(apiTagBank) + List(apiTagBank, apiTagBankAttribute, apiTagAttribute) ) lazy val deleteBankAttribute: OBPEndpoint = { From 2295e5e85615b38f192ea79ca7597c7118ae77b8 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:20:27 +0100 Subject: [PATCH 13/19] docfix: apiTagAccountAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala | 4 ++-- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 72ca399f5..aa615fd7c 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -21,6 +21,7 @@ object ApiTag { val apiTagBank = ResourceDocTag("Bank") val apiTagBankAttribute = ResourceDocTag("Bank-Attribute") val apiTagAccount = ResourceDocTag("Account") + val apiTagAccountAttribute = ResourceDocTag("Account-Attribute") val apiTagAccountAccess = ResourceDocTag("Account-Access") val apiTagDirectDebit = ResourceDocTag("Direct-Debit") val apiTagStandingOrder = ResourceDocTag("Standing-Order") diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 1b92566bd..8821d41fe 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -2667,7 +2667,7 @@ trait APIMethods310 { InvalidJsonFormat, UnknownError ), - List(apiTagAccount), + List(apiTagAccount, apiTagAccountAttribute, apiTagAttribute), Some(List(canCreateAccountAttributeAtOneBank)) ) @@ -2740,7 +2740,7 @@ trait APIMethods310 { InvalidJsonFormat, UnknownError ), - List(apiTagAccount), + List(apiTagAccount, apiTagAccountAttribute, apiTagAttribute), Some(List(canUpdateAccountAttribute)) ) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 4ca414ba6..0dff1203b 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -7988,7 +7988,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagAccount), + List(apiTagAccount, apiTagAccountAttribute, apiTagAttribute), Some(List(canCreateAccountAttributeDefinitionAtOneBank)) ) @@ -9243,7 +9243,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagAccount), + List(apiTagAccount, apiTagAccountAttribute, apiTagAttribute), Some(List(canDeleteAccountAttributeDefinitionAtOneBank)) ) @@ -9450,7 +9450,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagAccount), + List(apiTagAccount, apiTagAccountAttribute, apiTagAttribute), Some(List(canGetAccountAttributeDefinitionAtOneBank)) ) From 3e238b5cc971097f83ee009680c448c281459f86 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:23:12 +0100 Subject: [PATCH 14/19] docfix: apiTagProductAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + .../main/scala/code/api/v3_1_0/APIMethods310.scala | 8 ++++---- .../main/scala/code/api/v4_0_0/APIMethods400.scala | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index aa615fd7c..b66fe243b 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -62,6 +62,7 @@ object ApiTag { val apiTagATM = ResourceDocTag("ATM") val apiTagAtmAttribute = ResourceDocTag("ATM-Attribute") val apiTagProduct = ResourceDocTag("Product") + val apiTagProductAttribute = ResourceDocTag("Product-Attribute") val apiTagProductCollection = ResourceDocTag("Product-Collection") val apiTagOpenData = ResourceDocTag("Open-Data") val apiTagConsumer = ResourceDocTag("Consumer") diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 8821d41fe..925485e9d 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -1974,7 +1974,7 @@ trait APIMethods310 { InvalidJsonFormat, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canCreateProductAttribute)) ) @@ -2033,7 +2033,7 @@ trait APIMethods310 { UserHasMissingRoles, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canGetProductAttribute)) ) @@ -2075,7 +2075,7 @@ trait APIMethods310 { UserHasMissingRoles, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canUpdateProductAttribute)) ) @@ -2135,7 +2135,7 @@ trait APIMethods310 { BankNotFound, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canUpdateProductAttribute))) lazy val deleteProductAttribute : OBPEndpoint = { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 0dff1203b..9c65cd374 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -8068,7 +8068,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canCreateProductAttributeDefinitionAtOneBank)) ) @@ -8171,7 +8171,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canCreateProductAttribute)) ) @@ -8253,7 +8253,7 @@ trait APIMethods400 extends MdcLoggable { UserHasMissingRoles, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canUpdateProductAttribute)) ) @@ -8333,7 +8333,7 @@ trait APIMethods400 extends MdcLoggable { UserHasMissingRoles, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canUpdateProductAttribute)) ) @@ -9284,7 +9284,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canDeleteProductAttributeDefinitionAtOneBank)) ) @@ -9366,7 +9366,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagProduct), + List(apiTagProduct, apiTagProductAttribute, apiTagAttribute), Some(List(canGetProductAttributeDefinitionAtOneBank)) ) From 713e433eca506ecc63fd9c7ef070e7738d260eef Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 16 Dec 2025 23:25:28 +0100 Subject: [PATCH 15/19] docfix: apiTagCardAttribute and use of apiTagAttribute --- obp-api/src/main/scala/code/api/util/ApiTag.scala | 1 + obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala | 4 ++-- obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index b66fe243b..4fae15ec1 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -57,6 +57,7 @@ object ApiTag { val apiTagExperimental = ResourceDocTag("Experimental") val apiTagPerson = ResourceDocTag("Person") val apiTagCard = ResourceDocTag("Card") + val apiTagCardAttribute = ResourceDocTag("Card-Attribute") val apiTagSandbox = ResourceDocTag("Sandbox") val apiTagBranch = ResourceDocTag("Branch") val apiTagATM = ResourceDocTag("ATM") diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 925485e9d..b88d88b49 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -5147,7 +5147,7 @@ trait APIMethods310 { InvalidJsonFormat, UnknownError ), - List(apiTagCard)) + List(apiTagCard, apiTagCardAttribute, apiTagAttribute)) lazy val createCardAttribute : OBPEndpoint = { case "management"::"banks" :: bankId :: "cards" :: cardId :: "attribute" :: Nil JsonPost json -> _=> { @@ -5218,7 +5218,7 @@ trait APIMethods310 { InvalidJsonFormat, UnknownError ), - List(apiTagCard)) + List(apiTagCard, apiTagCardAttribute, apiTagAttribute)) lazy val updateCardAttribute : OBPEndpoint = { case "management"::"banks" :: bankId :: "cards" :: cardId :: "attributes" :: cardAttributeId :: Nil JsonPut json -> _=> { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 9c65cd374..78228ef5f 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -9084,7 +9084,7 @@ trait APIMethods400 extends MdcLoggable { InvalidJsonFormat, UnknownError ), - List(apiTagCard), + List(apiTagCard, apiTagCardAttribute, apiTagAttribute), Some(List(canCreateCardAttributeDefinitionAtOneBank)) ) @@ -9325,7 +9325,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagCard), + List(apiTagCard, apiTagCardAttribute, apiTagAttribute), Some(List(canDeleteCardAttributeDefinitionAtOneBank)) ) @@ -9539,7 +9539,7 @@ trait APIMethods400 extends MdcLoggable { $BankNotFound, UnknownError ), - List(apiTagCard), + List(apiTagCard, apiTagCardAttribute, apiTagAttribute), Some(List(canGetCardAttributeDefinitionAtOneBank)) ) From 0d69974941bb7d8dcc781d437c1b2493fe7236c9 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 17 Dec 2025 00:43:53 +0100 Subject: [PATCH 16/19] execute abac rule --- .../main/scala/code/api/v6_0_0/APIMethods600.scala | 14 +++++++------- .../scala/code/api/v6_0_0/JSONFactory6.0.0.scala | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 5485d87d5..7eecf7ac0 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4496,15 +4496,15 @@ trait APIMethods600 { | |""".stripMargin, ExecuteAbacRuleJsonV600( - authenticated_user_id = None, - on_behalf_of_user_id = None, - user_id = None, + authenticated_user_id = Some("c7b6cb47-cb96-4441-8801-35b57456753a"), + on_behalf_of_user_id = Some("a3b5c123-1234-5678-9012-fedcba987654"), + user_id = Some("c7b6cb47-cb96-4441-8801-35b57456753a"), bank_id = Some("gh.29.uk"), account_id = Some("8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"), - view_id = None, - transaction_id = None, - transaction_request_id = None, - customer_id = None + view_id = Some("owner"), + transaction_request_id = Some("123456"), + transaction_id = Some("abc123"), + customer_id = Some("customer-id-123") ), AbacRuleResultJsonV600( result = true diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 5af0070c2..697633eb2 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -324,8 +324,8 @@ case class ExecuteAbacRuleJsonV600( bank_id: Option[String], account_id: Option[String], view_id: Option[String], - transaction_id: Option[String], transaction_request_id: Option[String], + transaction_id: Option[String], customer_id: Option[String] ) From 1216add5c06db53200b8a62c5897892d5f240fba Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 17 Dec 2025 00:47:20 +0100 Subject: [PATCH 17/19] validate abac rule --- .../scala/code/api/v6_0_0/APIMethods600.scala | 100 ++++++++++++++++++ .../code/api/v6_0_0/JSONFactory6.0.0.scala | 20 ++++ 2 files changed, 120 insertions(+) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 7eecf7ac0..1cb30e5a8 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4473,6 +4473,106 @@ trait APIMethods600 { } } + staticResourceDocs += ResourceDoc( + validateAbacRule, + implementedInApiVersion, + nameOf(validateAbacRule), + "POST", + "/management/abac-rules/validate", + "Validate ABAC Rule", + s"""Validate ABAC rule code syntax and structure without creating or executing the rule. + | + |This endpoint performs the following validations: + |- Parse the rule_code as a Scala expression + |- Validate syntax - check for parsing errors + |- Validate field references - check if referenced objects/fields exist + |- Check type consistency - verify the expression returns a Boolean + | + |**Available ABAC Context Objects:** + |- AuthenticatedUser - The user who is logged in + |- OnBehalfOfUser - Optional delegation user + |- User - Target user being evaluated + |- Bank, Account, View, Transaction, TransactionRequest, Customer + |- Attributes for each entity (e.g., userAttributes, accountAttributes) + | + |**Documentation:** + |- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules + |- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters + |- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference + | + |This is a "dry-run" validation that does NOT save or execute the rule. + | + |${userAuthenticationMessage(true)} + | + |""".stripMargin, + ValidateAbacRuleJsonV600( + rule_code = """AuthenticatedUser.user_id == Account.owner_id""" + ), + ValidateAbacRuleSuccessJsonV600( + valid = true, + message = "ABAC rule code is valid" + ), + List( + UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagABAC), + Some(List(canCreateAbacRule)) + ) + + lazy val validateAbacRule: OBPEndpoint = { + case "management" :: "abac-rules" :: "validate" :: Nil JsonPost json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(user), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", user.userId, canCreateAbacRule, callContext) + validateJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) { + json.extract[ValidateAbacRuleJsonV600] + } + _ <- NewStyle.function.tryons(s"Rule code must not be empty", 400, callContext) { + validateJson.rule_code.trim.nonEmpty + } + validationResult <- Future { + AbacRuleEngine.validateRuleCode(validateJson.rule_code) match { + case Full(msg) => + Full(ValidateAbacRuleSuccessJsonV600( + valid = true, + message = msg + )) + case Failure(errorMsg, _, _) => + // Extract error details from the error message + val cleanError = errorMsg.replace("Invalid ABAC rule code: ", "").replace("Failed to compile ABAC rule: ", "") + Full(ValidateAbacRuleFailureJsonV600( + valid = false, + error = cleanError, + message = "Rule validation failed", + details = ValidateAbacRuleErrorDetailsJsonV600( + error_type = if (cleanError.toLowerCase.contains("syntax")) "SyntaxError" + else if (cleanError.toLowerCase.contains("type")) "TypeError" + else "CompilationError" + ) + )) + case Empty => + Full(ValidateAbacRuleFailureJsonV600( + valid = false, + error = "Unknown validation error", + message = "Rule validation failed", + details = ValidateAbacRuleErrorDetailsJsonV600( + error_type = "UnknownError" + ) + )) + } + } map { + unboxFullOrFail(_, callContext, "Validation failed", 400) + } + } yield { + (validationResult, HttpCode.`200`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( executeAbacRule, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 697633eb2..f789371e0 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -333,6 +333,26 @@ case class AbacRuleResultJsonV600( result: Boolean ) +case class ValidateAbacRuleJsonV600( + rule_code: String +) + +case class ValidateAbacRuleSuccessJsonV600( + valid: Boolean, + message: String +) + +case class ValidateAbacRuleErrorDetailsJsonV600( + error_type: String +) + +case class ValidateAbacRuleFailureJsonV600( + valid: Boolean, + error: String, + message: String, + details: ValidateAbacRuleErrorDetailsJsonV600 +) + object JSONFactory600 extends CustomJsonFormats with MdcLoggable{ def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = { From 13d3e9b46488d708dcb3deaa7e60fc7b312fc2bc Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 17 Dec 2025 01:27:05 +0100 Subject: [PATCH 18/19] abac schema update --- .../scala/code/api/v6_0_0/APIMethods600.scala | 227 ++++++++++++++++++ .../code/api/v6_0_0/JSONFactory6.0.0.scala | 28 +++ 2 files changed, 255 insertions(+) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 1cb30e5a8..58bd1fe49 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4473,6 +4473,233 @@ trait APIMethods600 { } } + staticResourceDocs += ResourceDoc( + getAbacRuleSchema, + implementedInApiVersion, + nameOf(getAbacRuleSchema), + "GET", + "/management/abac-rules/schema", + "Get ABAC Rule Schema", + s"""Get schema information about ABAC rule structure for building rule code. + | + |This endpoint returns schema information including: + |- All 18 parameters available in ABAC rules + |- Object types (User, Bank, Account, etc.) and their properties + |- Available operators and syntax + |- Example rules + | + |This schema information is useful for: + |- Building rule editors with auto-completion + |- Validating rule syntax in frontends + |- AI agents that help construct rules + |- Dynamic form builders + | + |${userAuthenticationMessage(true)} + | + |""".stripMargin, + EmptyBody, + AbacRuleSchemaJsonV600( + parameters = List( + AbacParameterJsonV600( + name = "authenticatedUser", + `type` = "User", + description = "The logged-in user (always present)", + required = true, + category = "User" + ) + ), + object_types = List( + AbacObjectTypeJsonV600( + name = "User", + description = "User object with profile information", + properties = List( + AbacObjectPropertyJsonV600( + name = "userId", + `type` = "String", + description = "Unique user ID" + ) + ) + ) + ), + examples = List( + "authenticatedUser.userId == user.userId", + "bankOpt.isDefined && bankOpt.get.bankId.value == \"gh.29.uk\"" + ), + available_operators = List("==", "!=", "&&", "||", "!", ">", "<", ">=", "<=", "contains", "isDefined"), + notes = List( + "Only authenticatedUser is guaranteed to exist (not wrapped in Option)", + "All other objects are Option types - use isDefined or pattern matching", + "Attributes are Lists - use .find(), .exists(), .forall() etc." + ) + ), + List( + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagABAC), + Some(List(canGetAbacRule)) + ) + + lazy val getAbacRuleSchema: OBPEndpoint = { + case "management" :: "abac-rules" :: "schema" :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(user), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext) + } yield { + val metadata = AbacRuleSchemaJsonV600( + parameters = List( + AbacParameterJsonV600("authenticatedUser", "User", "The logged-in user (always present)", required = true, "User"), + AbacParameterJsonV600("authenticatedUserAttributes", "List[UserAttributeTrait]", "Non-personal attributes of authenticated user", required = true, "User"), + AbacParameterJsonV600("authenticatedUserAuthContext", "List[UserAuthContext]", "Auth context of authenticated user", required = true, "User"), + AbacParameterJsonV600("onBehalfOfUserOpt", "Option[User]", "User being acted on behalf of (delegation)", required = false, "User"), + AbacParameterJsonV600("onBehalfOfUserAttributes", "List[UserAttributeTrait]", "Attributes of delegation user", required = false, "User"), + AbacParameterJsonV600("onBehalfOfUserAuthContext", "List[UserAuthContext]", "Auth context of delegation user", required = false, "User"), + AbacParameterJsonV600("userOpt", "Option[User]", "Target user being evaluated", required = false, "User"), + AbacParameterJsonV600("userAttributes", "List[UserAttributeTrait]", "Attributes of target user", required = false, "User"), + AbacParameterJsonV600("bankOpt", "Option[Bank]", "Bank context", required = false, "Bank"), + AbacParameterJsonV600("bankAttributes", "List[BankAttributeTrait]", "Bank attributes", required = false, "Bank"), + AbacParameterJsonV600("accountOpt", "Option[BankAccount]", "Account context", required = false, "Account"), + AbacParameterJsonV600("accountAttributes", "List[AccountAttribute]", "Account attributes", required = false, "Account"), + AbacParameterJsonV600("transactionOpt", "Option[Transaction]", "Transaction context", required = false, "Transaction"), + AbacParameterJsonV600("transactionAttributes", "List[TransactionAttribute]", "Transaction attributes", required = false, "Transaction"), + AbacParameterJsonV600("transactionRequestOpt", "Option[TransactionRequest]", "Transaction request context", required = false, "TransactionRequest"), + AbacParameterJsonV600("transactionRequestAttributes", "List[TransactionRequestAttributeTrait]", "Transaction request attributes", required = false, "TransactionRequest"), + AbacParameterJsonV600("customerOpt", "Option[Customer]", "Customer context", required = false, "Customer"), + AbacParameterJsonV600("customerAttributes", "List[CustomerAttribute]", "Customer attributes", required = false, "Customer") + ), + object_types = List( + AbacObjectTypeJsonV600("User", "User object with profile and authentication information", List( + AbacObjectPropertyJsonV600("userId", "String", "Unique user ID"), + AbacObjectPropertyJsonV600("emailAddress", "String", "User email address"), + AbacObjectPropertyJsonV600("provider", "String", "Authentication provider (e.g., 'obp')"), + AbacObjectPropertyJsonV600("name", "String", "User display name"), + AbacObjectPropertyJsonV600("idGivenByProvider", "String", "ID given by provider (same as username)"), + AbacObjectPropertyJsonV600("createdByConsentId", "Option[String]", "Consent ID that created the user (if any)"), + AbacObjectPropertyJsonV600("isDeleted", "Option[Boolean]", "Whether user is deleted") + )), + AbacObjectTypeJsonV600("Bank", "Bank object", List( + AbacObjectPropertyJsonV600("bankId", "BankId", "Bank ID"), + AbacObjectPropertyJsonV600("fullName", "String", "Bank full name"), + AbacObjectPropertyJsonV600("shortName", "String", "Bank short name"), + AbacObjectPropertyJsonV600("logoUrl", "String", "Bank logo URL"), + AbacObjectPropertyJsonV600("websiteUrl", "String", "Bank website URL"), + AbacObjectPropertyJsonV600("bankRoutingScheme", "String", "Bank routing scheme"), + AbacObjectPropertyJsonV600("bankRoutingAddress", "String", "Bank routing address") + )), + AbacObjectTypeJsonV600("BankAccount", "Bank account object", List( + AbacObjectPropertyJsonV600("accountId", "AccountId", "Account ID"), + AbacObjectPropertyJsonV600("bankId", "BankId", "Bank ID"), + AbacObjectPropertyJsonV600("accountType", "String", "Account type"), + AbacObjectPropertyJsonV600("balance", "BigDecimal", "Account balance"), + AbacObjectPropertyJsonV600("currency", "String", "Account currency"), + AbacObjectPropertyJsonV600("name", "String", "Account name"), + AbacObjectPropertyJsonV600("label", "String", "Account label"), + AbacObjectPropertyJsonV600("number", "String", "Account number"), + AbacObjectPropertyJsonV600("lastUpdate", "Date", "Last update date"), + AbacObjectPropertyJsonV600("branchId", "String", "Branch ID"), + AbacObjectPropertyJsonV600("accountRoutings", "List[AccountRouting]", "Account routings") + )), + AbacObjectTypeJsonV600("Transaction", "Transaction object", List( + AbacObjectPropertyJsonV600("id", "TransactionId", "Transaction ID"), + AbacObjectPropertyJsonV600("uuid", "String", "Universally unique ID"), + AbacObjectPropertyJsonV600("thisAccount", "BankAccount", "This account"), + AbacObjectPropertyJsonV600("otherAccount", "Counterparty", "Other account/counterparty"), + AbacObjectPropertyJsonV600("transactionType", "String", "Transaction type (e.g., cash withdrawal)"), + AbacObjectPropertyJsonV600("amount", "BigDecimal", "Transaction amount"), + AbacObjectPropertyJsonV600("currency", "String", "Transaction currency (ISO 4217)"), + AbacObjectPropertyJsonV600("description", "Option[String]", "Bank provided label"), + AbacObjectPropertyJsonV600("startDate", "Date", "Date transaction was initiated"), + AbacObjectPropertyJsonV600("finishDate", "Option[Date]", "Date money finished changing hands"), + AbacObjectPropertyJsonV600("balance", "BigDecimal", "New balance after transaction"), + AbacObjectPropertyJsonV600("status", "Option[String]", "Transaction status") + )), + AbacObjectTypeJsonV600("TransactionRequest", "Transaction request object", List( + AbacObjectPropertyJsonV600("id", "TransactionRequestId", "Transaction request ID"), + AbacObjectPropertyJsonV600("type", "String", "Transaction request type"), + AbacObjectPropertyJsonV600("from", "TransactionRequestAccount", "From account"), + AbacObjectPropertyJsonV600("status", "String", "Transaction request status"), + AbacObjectPropertyJsonV600("start_date", "Date", "Start date"), + AbacObjectPropertyJsonV600("end_date", "Date", "End date"), + AbacObjectPropertyJsonV600("transaction_ids", "String", "Associated transaction IDs"), + AbacObjectPropertyJsonV600("charge", "TransactionRequestCharge", "Charge information"), + AbacObjectPropertyJsonV600("this_bank_id", "BankId", "This bank ID"), + AbacObjectPropertyJsonV600("this_account_id", "AccountId", "This account ID"), + AbacObjectPropertyJsonV600("counterparty_id", "CounterpartyId", "Counterparty ID") + )), + AbacObjectTypeJsonV600("Customer", "Customer object", List( + AbacObjectPropertyJsonV600("customerId", "String", "Customer ID (UUID)"), + AbacObjectPropertyJsonV600("bankId", "String", "Bank ID"), + AbacObjectPropertyJsonV600("number", "String", "Customer number (bank identifier)"), + AbacObjectPropertyJsonV600("legalName", "String", "Customer legal name"), + AbacObjectPropertyJsonV600("mobileNumber", "String", "Customer mobile number"), + AbacObjectPropertyJsonV600("email", "String", "Customer email"), + AbacObjectPropertyJsonV600("dateOfBirth", "Date", "Date of birth"), + AbacObjectPropertyJsonV600("relationshipStatus", "String", "Relationship status"), + AbacObjectPropertyJsonV600("dependents", "Integer", "Number of dependents") + )), + AbacObjectTypeJsonV600("UserAttributeTrait", "User attribute", List( + AbacObjectPropertyJsonV600("name", "String", "Attribute name"), + AbacObjectPropertyJsonV600("value", "String", "Attribute value"), + AbacObjectPropertyJsonV600("attributeType", "AttributeType", "Attribute type (STRING, INTEGER, DOUBLE, DATE_WITH_DAY)") + )), + AbacObjectTypeJsonV600("AccountAttribute", "Account attribute", List( + AbacObjectPropertyJsonV600("name", "String", "Attribute name"), + AbacObjectPropertyJsonV600("value", "String", "Attribute value"), + AbacObjectPropertyJsonV600("attributeType", "AttributeType", "Attribute type") + )), + AbacObjectTypeJsonV600("TransactionAttribute", "Transaction attribute", List( + AbacObjectPropertyJsonV600("name", "String", "Attribute name"), + AbacObjectPropertyJsonV600("value", "String", "Attribute value"), + AbacObjectPropertyJsonV600("attributeType", "AttributeType", "Attribute type") + )), + AbacObjectTypeJsonV600("CustomerAttribute", "Customer attribute", List( + AbacObjectPropertyJsonV600("name", "String", "Attribute name"), + AbacObjectPropertyJsonV600("value", "String", "Attribute value"), + AbacObjectPropertyJsonV600("attributeType", "AttributeType", "Attribute type") + )) + ), + examples = List( + "// Check if authenticated user matches target user", + "authenticatedUser.userId == userOpt.get.userId", + "// Check user email contains admin", + "authenticatedUser.emailAddress.contains(\"admin\")", + "// Check specific bank", + "bankOpt.isDefined && bankOpt.get.bankId.value == \"gh.29.uk\"", + "// Check account balance", + "accountOpt.isDefined && accountOpt.get.balance > 1000", + "// Check user attributes", + "userAttributes.exists(attr => attr.name == \"account_type\" && attr.value == \"premium\")", + "// Check authenticated user has role attribute", + "authenticatedUserAttributes.find(_.name == \"role\").exists(_.value == \"admin\")", + "// IMPORTANT: Use camelCase (userId NOT user_id)", + "// IMPORTANT: Parameters are: authenticatedUser, userOpt, accountOpt (with Opt suffix for Optional)", + "// IMPORTANT: Check isDefined before using .get on Option types" + ), + available_operators = List( + "==", "!=", "&&", "||", "!", ">", "<", ">=", "<=", + "contains", "startsWith", "endsWith", + "isDefined", "isEmpty", "nonEmpty", + "exists", "forall", "find", "filter", + "get", "getOrElse" + ), + notes = List( + "PARAMETER NAMES: Use authenticatedUser, userOpt, accountOpt, bankOpt, transactionOpt, etc. (NOT user, account, bank)", + "PROPERTY NAMES: Use camelCase - userId (NOT user_id), accountId (NOT account_id), emailAddress (NOT email_address)", + "OPTION TYPES: Only authenticatedUser is guaranteed to exist. All others are Option types - check isDefined before using .get", + "ATTRIBUTES: All attributes are Lists - use Scala collection methods like exists(), find(), filter()", + "SAFE OPTION HANDLING: Use pattern matching: userOpt match { case Some(u) => u.userId == ... case None => false }", + "RETURN TYPE: Rule must return Boolean - true = access granted, false = access denied", + "AUTO-FETCHING: Objects are automatically fetched based on IDs passed to execute endpoint", + "COMMON MISTAKE: Writing 'user.user_id' instead of 'userOpt.get.userId' or 'authenticatedUser.userId'" + ) + ) + (metadata, HttpCode.`200`(callContext)) + } + } + } + staticResourceDocs += ResourceDoc( validateAbacRule, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index f789371e0..a54513d0d 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -353,6 +353,34 @@ case class ValidateAbacRuleFailureJsonV600( details: ValidateAbacRuleErrorDetailsJsonV600 ) +case class AbacParameterJsonV600( + name: String, + `type`: String, + description: String, + required: Boolean, + category: String +) + +case class AbacObjectPropertyJsonV600( + name: String, + `type`: String, + description: String +) + +case class AbacObjectTypeJsonV600( + name: String, + description: String, + properties: List[AbacObjectPropertyJsonV600] +) + +case class AbacRuleSchemaJsonV600( + parameters: List[AbacParameterJsonV600], + object_types: List[AbacObjectTypeJsonV600], + examples: List[String], + available_operators: List[String], + notes: List[String] +) + object JSONFactory600 extends CustomJsonFormats with MdcLoggable{ def createCurrentUsageJson(rateLimits: List[((Option[Long], Option[Long]), LimitCallPeriod)]): Option[RedisCallLimitJson] = { From 1779d6b3154cc8995a79ed57a6fc1411f9978fff Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 17 Dec 2025 01:58:32 +0100 Subject: [PATCH 19/19] endpoint: GET /obp/v6.0.0/management/abac-rules-schema --- obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 58bd1fe49..559dcdf30 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -4478,7 +4478,7 @@ trait APIMethods600 { implementedInApiVersion, nameOf(getAbacRuleSchema), "GET", - "/management/abac-rules/schema", + "/management/abac-rules-schema", "Get ABAC Rule Schema", s"""Get schema information about ABAC rule structure for building rule code. | @@ -4542,7 +4542,7 @@ trait APIMethods600 { ) lazy val getAbacRuleSchema: OBPEndpoint = { - case "management" :: "abac-rules" :: "schema" :: Nil JsonGet _ => { + case "management" :: "abac-rules-schema" :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { (Full(user), callContext) <- authenticatedAccess(cc)