mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
Create REPLACE_USER_ID_WITH_CONSENT_USER_ID.md
This commit is contained in:
parent
e9a1927bed
commit
47c6e4416d
560
REPLACE_USER_ID_WITH_CONSENT_USER_ID.md
Normal file
560
REPLACE_USER_ID_WITH_CONSENT_USER_ID.md
Normal file
@ -0,0 +1,560 @@
|
||||
# Replacing User ID with Consent User ID at Connector Level
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains where and how to replace the authenticated user's `user_id` with a `user_id` from a consent at the connector level in the OBP-API. This replacement should occur after security guards (authentication/authorization) but just before database operations or external messaging (RabbitMQ, REST, etc.).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Key Concepts](#key-concepts)
|
||||
3. [Where to Make Changes](#where-to-make-changes)
|
||||
4. [Implementation Guide](#implementation-guide)
|
||||
5. [Important Considerations](#important-considerations)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Call Flow
|
||||
|
||||
```
|
||||
API Endpoint
|
||||
↓
|
||||
Authentication/Authorization (Security Guards)
|
||||
↓
|
||||
CallContext (contains user + consenter)
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ THIS IS WHERE USER_ID REPLACEMENT HAPPENS │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
├──→ External Connectors (RabbitMQ, REST, Akka, etc.)
|
||||
│ ↓
|
||||
│ toOutboundAdapterCallContext()
|
||||
│ ↓
|
||||
│ External Adapter/System
|
||||
│
|
||||
└──→ LocalMappedConnector (Built-in Database)
|
||||
↓
|
||||
Direct Database Operations
|
||||
```
|
||||
|
||||
### CallContext Structure
|
||||
|
||||
The `CallContext` class (in `code.api.util.ApiSession`) contains:
|
||||
|
||||
```scala
|
||||
case class CallContext(
|
||||
user: Box[User] = Empty, // The authenticated user
|
||||
consenter: Box[User] = Empty, // The user from consent (if present)
|
||||
consumer: Box[Consumer] = Empty,
|
||||
// ... other fields
|
||||
)
|
||||
```
|
||||
|
||||
When Berlin Group consents are applied, the `consenter` field is populated with the consent user:
|
||||
|
||||
```scala
|
||||
// From ConsentUtil.scala line 596
|
||||
val updatedCallContext = callContext.copy(consenter = user)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. Consent User vs Authenticated User
|
||||
|
||||
- **Authenticated User**: The user/application that made the API request (in `callContext.user`)
|
||||
- **Consent User**: The account holder who gave consent for access (in `callContext.consenter`)
|
||||
|
||||
In consent-based scenarios (e.g., Berlin Group PSD2), a TPP (Third Party Provider) authenticates, but operates on behalf of the PSU (Payment Service User) who gave consent.
|
||||
|
||||
### 2. Connector Types
|
||||
|
||||
#### External Connectors
|
||||
|
||||
- **RabbitMQConnector_vOct2024**
|
||||
- **RestConnector_vMar2019**
|
||||
- **AkkaConnector_vDec2018**
|
||||
- **StoredProcedureConnector_vDec2019**
|
||||
- **EthereumConnector_vSept2025**
|
||||
- **CardanoConnector_vJun2025**
|
||||
|
||||
These send messages to external adapters/systems and use `OutboundAdapterCallContext`.
|
||||
|
||||
#### Internal Connector
|
||||
|
||||
- **LocalMappedConnector**
|
||||
|
||||
Works directly with the OBP database (Mapper/ORM layer) and does NOT use `OutboundAdapterCallContext`.
|
||||
|
||||
---
|
||||
|
||||
## Where to Make Changes
|
||||
|
||||
### For External Connectors
|
||||
|
||||
**Location**: `OBP-API/obp-api/src/main/scala/code/api/util/ApiSession.scala`
|
||||
|
||||
**Method**: `CallContext.toOutboundAdapterCallContext` (lines 65-115)
|
||||
|
||||
This is the **single transformation point** where `CallContext` is converted to `OutboundAdapterCallContext` before being sent to external systems.
|
||||
|
||||
Example of how it's used in connectors:
|
||||
|
||||
```scala
|
||||
// From RabbitMQConnector_vOct2024.scala line 2204
|
||||
override def makePaymentv210(..., callContext: Option[CallContext]): ... = {
|
||||
val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, ...)
|
||||
val response: Future[Box[InBound]] = sendRequest[InBound]("obp_make_paymentv210", req, callContext)
|
||||
}
|
||||
```
|
||||
|
||||
### For LocalMappedConnector
|
||||
|
||||
**Challenge**: LocalMappedConnector does NOT use `toOutboundAdapterCallContext`. It works directly with database entities.
|
||||
|
||||
**Key Location**: `OBP-API/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala`
|
||||
|
||||
Methods like `savePayment` (line 2223) and `getBankAccountsForUserLegacy` (line 624) work directly with parameters and database objects.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### Option 1: Modify toOutboundAdapterCallContext (External Connectors Only)
|
||||
|
||||
This approach works for RabbitMQ, REST, Akka, and other external connectors.
|
||||
|
||||
#### Current Implementation
|
||||
|
||||
```scala
|
||||
// ApiSession.scala lines 65-115
|
||||
def toOutboundAdapterCallContext: OutboundAdapterCallContext = {
|
||||
for {
|
||||
user <- this.user
|
||||
username <- tryo(Some(user.name))
|
||||
currentResourceUserId <- tryo(Some(user.userId)) // <-- Uses authenticated user
|
||||
consumerId = this.consumer.map(_.consumerId.get).openOr("")
|
||||
permission <- Views.views.vend.getPermissionForUser(user)
|
||||
views <- tryo(permission.views)
|
||||
linkedCustomers <- tryo(CustomerX.customerProvider.vend.getCustomersByUserId(user.userId))
|
||||
// ...
|
||||
OutboundAdapterCallContext(
|
||||
correlationId = this.correlationId,
|
||||
sessionId = this.sessionId,
|
||||
consumerId = Some(consumerId),
|
||||
generalContext = Some(generalContextFromPassThroughHeaders),
|
||||
outboundAdapterAuthInfo = Some(OutboundAdapterAuthInfo(
|
||||
userId = currentResourceUserId, // <-- Authenticated user's ID sent to adapter
|
||||
username = username,
|
||||
linkedCustomers = likedCustomersBasic,
|
||||
userAuthContext = basicUserAuthContexts,
|
||||
if (authViews.isEmpty) None else Some(authViews))),
|
||||
outboundAdapterConsenterInfo =
|
||||
if (this.consenter.isDefined){
|
||||
Some(OutboundAdapterAuthInfo(
|
||||
username = this.consenter.toOption.map(_.name)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Proposed Change
|
||||
|
||||
```scala
|
||||
def toOutboundAdapterCallContext: OutboundAdapterCallContext = {
|
||||
for {
|
||||
user <- this.user
|
||||
|
||||
// Determine the effective user: use consenter if present, otherwise authenticated user
|
||||
val effectiveUser = this.consenter.toOption.getOrElse(user)
|
||||
|
||||
username <- tryo(Some(effectiveUser.name))
|
||||
currentResourceUserId <- tryo(Some(effectiveUser.userId)) // <-- NOW uses consent user if present
|
||||
consumerId = this.consumer.map(_.consumerId.get).openOr("")
|
||||
|
||||
// Use effectiveUser for permissions and linked data
|
||||
permission <- Views.views.vend.getPermissionForUser(effectiveUser)
|
||||
views <- tryo(permission.views)
|
||||
linkedCustomers <- tryo(CustomerX.customerProvider.vend.getCustomersByUserId(effectiveUser.userId))
|
||||
likedCustomersBasic = if (linkedCustomers.isEmpty) None else Some(createInternalLinkedBasicCustomersJson(linkedCustomers))
|
||||
userAuthContexts <- UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(effectiveUser.userId)
|
||||
basicUserAuthContextsFromDatabase = if (userAuthContexts.isEmpty) None else Some(createBasicUserAuthContextJson(userAuthContexts))
|
||||
generalContextFromPassThroughHeaders = createBasicUserAuthContextJsonFromCallContext(this)
|
||||
basicUserAuthContexts = Some(basicUserAuthContextsFromDatabase.getOrElse(List.empty[BasicUserAuthContext]))
|
||||
authViews <- tryo(
|
||||
for {
|
||||
view <- views
|
||||
(account, callContext) <- code.bankconnectors.LocalMappedConnector.getBankAccountLegacy(view.bankId, view.accountId, Some(this)) ?~! {BankAccountNotFound}
|
||||
internalCustomers = createAuthInfoCustomersJson(account.customerOwners.toList)
|
||||
internalUsers = createAuthInfoUsersJson(account.userOwners.toList)
|
||||
viewBasic = ViewBasic(view.viewId.value, view.name, view.description)
|
||||
accountBasic = AccountBasic(
|
||||
account.accountId.value,
|
||||
account.accountRoutings,
|
||||
internalCustomers.customers,
|
||||
internalUsers.users)
|
||||
} yield
|
||||
AuthView(viewBasic, accountBasic)
|
||||
)
|
||||
} yield {
|
||||
OutboundAdapterCallContext(
|
||||
correlationId = this.correlationId,
|
||||
sessionId = this.sessionId,
|
||||
consumerId = Some(consumerId),
|
||||
generalContext = Some(generalContextFromPassThroughHeaders),
|
||||
outboundAdapterAuthInfo = Some(OutboundAdapterAuthInfo(
|
||||
userId = currentResourceUserId, // <-- Now contains consent user's ID
|
||||
username = username, // <-- Now contains consent user's name
|
||||
linkedCustomers = likedCustomersBasic,
|
||||
userAuthContext = basicUserAuthContexts,
|
||||
if (authViews.isEmpty) None else Some(authViews))),
|
||||
outboundAdapterConsenterInfo =
|
||||
if (this.consenter.isDefined) {
|
||||
Some(OutboundAdapterAuthInfo(
|
||||
userId = Some(this.consenter.toOption.get.userId), // <-- ADD this
|
||||
username = this.consenter.toOption.map(_.name)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
}}.openOr(OutboundAdapterCallContext(
|
||||
this.correlationId,
|
||||
this.sessionId))
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Add Helper Method (For Both Internal and External)
|
||||
|
||||
Add a convenience method to `CallContext` that returns the effective user:
|
||||
|
||||
```scala
|
||||
// Add to CallContext class in ApiSession.scala
|
||||
case class CallContext(
|
||||
// ... existing fields
|
||||
) {
|
||||
// ... existing methods
|
||||
|
||||
/**
|
||||
* Returns the consent user if present, otherwise returns the authenticated user.
|
||||
* Use this method when you need the "effective" user for operations.
|
||||
*/
|
||||
def effectiveUser: Box[User] = consenter.or(user)
|
||||
|
||||
/**
|
||||
* Returns the user ID of the effective user (consent user if present, otherwise authenticated user).
|
||||
* Throws exception if no user is available.
|
||||
*/
|
||||
def effectiveUserId: String = effectiveUser.map(_.userId).openOrThrowException(UserNotLoggedIn)
|
||||
|
||||
// ... rest of class
|
||||
}
|
||||
```
|
||||
|
||||
Then use throughout the codebase:
|
||||
|
||||
```scala
|
||||
// In LocalMappedConnector or other places
|
||||
override def getBankAccountsForUserLegacy(..., callContext: Option[CallContext]): ... = {
|
||||
// Instead of getting user from parameters
|
||||
val userId = callContext.map(_.effectiveUserId).getOrElse(...)
|
||||
val userAuthContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(userId)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: Hybrid Approach (Recommended)
|
||||
|
||||
Combine both options:
|
||||
|
||||
1. **For External Connectors**: Implement the change in `toOutboundAdapterCallContext` (Option 1)
|
||||
2. **For Internal Code**: Add helper methods (Option 2) and use them where appropriate
|
||||
3. **At API Layer**: Consider handling critical consent-based operations at the endpoint level before calling connectors
|
||||
|
||||
---
|
||||
|
||||
## Important Considerations
|
||||
|
||||
### 1. LocalMappedConnector Limitations
|
||||
|
||||
The `LocalMappedConnector` typically doesn't use `CallContext` for user information in its core transaction methods. For example:
|
||||
|
||||
```scala
|
||||
// LocalMappedConnectorInternal.scala line 613
|
||||
def saveTransaction(
|
||||
fromAccount: BankAccount,
|
||||
toAccount: BankAccount,
|
||||
// ... other parameters
|
||||
// NOTE: No callContext parameter!
|
||||
): Box[TransactionId] = {
|
||||
// Creates transaction directly from account objects
|
||||
mappedTransaction <- tryo(MappedTransaction.create
|
||||
.bank(fromAccount.bankId.value)
|
||||
.account(fromAccount.accountId.value)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The user information is embedded in the `BankAccount` objects passed to these methods, not extracted from `CallContext`.
|
||||
|
||||
### 2. Consent Flow
|
||||
|
||||
The consent user is set in `ConsentUtil.scala`:
|
||||
|
||||
```scala
|
||||
// ConsentUtil.scala line 596
|
||||
case Full(storedConsent) =>
|
||||
val user = Users.users.vend.getUserByUserId(storedConsent.userId)
|
||||
val updatedCallContext = callContext.copy(consenter = user)
|
||||
```
|
||||
|
||||
This happens during Berlin Group consent validation, so `callContext.consenter` will only be populated for consent-based requests.
|
||||
|
||||
### 3. OutboundAdapterAuthInfo Structure
|
||||
|
||||
The structure sent to external adapters:
|
||||
|
||||
```scala
|
||||
// From CommonModel.scala line 1221
|
||||
case class OutboundAdapterAuthInfo(
|
||||
userId: Option[String] = None, // Main user ID
|
||||
username: Option[String] = None, // Main username
|
||||
linkedCustomers: Option[List[BasicLinkedCustomer]] = None,
|
||||
userAuthContext: Option[List[BasicUserAuthContext]] = None,
|
||||
authViews: Option[List[AuthView]] = None,
|
||||
)
|
||||
|
||||
// And in OutboundAdapterCallContext line 1207
|
||||
case class OutboundAdapterCallContext(
|
||||
correlationId: String = "",
|
||||
sessionId: Option[String] = None,
|
||||
consumerId: Option[String] = None,
|
||||
generalContext: Option[List[BasicGeneralContext]] = None,
|
||||
outboundAdapterAuthInfo: Option[OutboundAdapterAuthInfo] = None, // Main user
|
||||
outboundAdapterConsenterInfo: Option[OutboundAdapterAuthInfo] = None, // Consent user
|
||||
)
|
||||
```
|
||||
|
||||
Currently, `outboundAdapterAuthInfo` contains the authenticated user, and `outboundAdapterConsenterInfo` contains minimal consent user info. After the proposed change, `outboundAdapterAuthInfo` would contain the consent user when present.
|
||||
|
||||
### 4. Security Implications
|
||||
|
||||
**IMPORTANT**: This change means that operations will be performed using the consent user's identity rather than the authenticated user's identity. Ensure:
|
||||
|
||||
- Security guards have already validated that the authenticated user has permission to act on behalf of the consent user
|
||||
- Audit logs capture both the authenticated user (who made the request) and the effective user (whose account is being accessed)
|
||||
- The consent is valid and not expired before this transformation happens
|
||||
|
||||
### 5. Backward Compatibility
|
||||
|
||||
When implementing this change:
|
||||
|
||||
- Existing requests without consent should continue to work (use authenticated user)
|
||||
- External adapters might need updates if they rely on specific user ID mappings
|
||||
- Consider adding a feature flag to enable/disable this behavior during testing
|
||||
|
||||
### 6. Testing Strategy
|
||||
|
||||
Test scenarios:
|
||||
|
||||
1. **No Consent**: Request with authenticated user only → should use authenticated user's ID
|
||||
2. **With Valid Consent**: Request with consent → should use consent user's ID
|
||||
3. **Expired Consent**: Should fail before reaching connector level
|
||||
4. **Mixed Operations**: Some endpoints with consent, some without → each should use correct user
|
||||
5. **External vs Internal**: Verify both external connectors and LocalMappedConnector behave correctly
|
||||
|
||||
### 7. Dynamic Entities
|
||||
|
||||
**Challenge**: Dynamic Entities pass `userId` explicitly as a parameter rather than relying solely on `CallContext`.
|
||||
|
||||
**Location**: `OBP-API/obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala`
|
||||
|
||||
#### Current Implementation
|
||||
|
||||
In the Dynamic Entity generic endpoint (line 132):
|
||||
|
||||
```scala
|
||||
(box, _) <- NewStyle.function.invokeDynamicConnector(
|
||||
operation,
|
||||
entityName,
|
||||
None,
|
||||
Option(id).filter(StringUtils.isNotBlank),
|
||||
bankId,
|
||||
None,
|
||||
Some(u.userId), // <-- Uses authenticated user's ID directly
|
||||
isPersonalEntity,
|
||||
Some(cc)
|
||||
)
|
||||
```
|
||||
|
||||
The `userId` parameter is extracted from the authenticated user (`u.userId`) and passed directly to the connector.
|
||||
|
||||
#### Flow Through to Connector
|
||||
|
||||
This userId flows through:
|
||||
|
||||
1. **NewStyle.function.invokeDynamicConnector** (ApiUtil.scala line 3372)
|
||||
2. **Connector.dynamicEntityProcess** (Connector.scala line 1766)
|
||||
3. **LocalMappedConnector.dynamicEntityProcess** (LocalMappedConnector.scala line 4324)
|
||||
4. **DynamicDataProvider methods** (DynamicDataProvider.scala line 35-43)
|
||||
|
||||
Example in LocalMappedConnector:
|
||||
|
||||
```scala
|
||||
case GET_ALL => Full {
|
||||
val dataList = DynamicDataProvider.connectorMethodProvider.vend
|
||||
.getAllDataJson(bankId, entityName, userId, isPersonalEntity) // <-- userId used here
|
||||
JArray(dataList)
|
||||
}
|
||||
```
|
||||
|
||||
#### Proposed Solution
|
||||
|
||||
**Option A: Use effectiveUserId in API Layer**
|
||||
|
||||
Modify `APIMethodsDynamicEntity.scala` to use the consent user when present:
|
||||
|
||||
```scala
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(callContext)
|
||||
|
||||
// Determine effective user: consent user if present, otherwise authenticated user
|
||||
effectiveUserId = callContext.consenter.map(_.userId).openOr(u.userId)
|
||||
|
||||
// ... other validations ...
|
||||
|
||||
(box, _) <- NewStyle.function.invokeDynamicConnector(
|
||||
operation,
|
||||
entityName,
|
||||
None,
|
||||
Option(id).filter(StringUtils.isNotBlank),
|
||||
bankId,
|
||||
None,
|
||||
Some(effectiveUserId), // <-- Now uses consent user if available
|
||||
isPersonalEntity,
|
||||
Some(cc)
|
||||
)
|
||||
} yield {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Option B: Add Helper to CallContext (Recommended)**
|
||||
|
||||
Use the `effectiveUserId` helper method proposed in Option 2:
|
||||
|
||||
```scala
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(callContext)
|
||||
|
||||
// ... other validations ...
|
||||
|
||||
(box, _) <- NewStyle.function.invokeDynamicConnector(
|
||||
operation,
|
||||
entityName,
|
||||
None,
|
||||
Option(id).filter(StringUtils.isNotBlank),
|
||||
bankId,
|
||||
None,
|
||||
Some(callContext.effectiveUserId), // <-- Uses helper method
|
||||
isPersonalEntity,
|
||||
Some(cc)
|
||||
)
|
||||
} yield {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Impact Analysis
|
||||
|
||||
Dynamic Entities use `userId` for:
|
||||
|
||||
1. **Personal Entities (`isPersonalEntity = true`)**: Scoping data to specific users
|
||||
- GET_ALL: Filters data by userId
|
||||
- GET_ONE: Validates ownership by userId
|
||||
- CREATE: Associates new data with userId
|
||||
- UPDATE/DELETE: Validates user owns the data
|
||||
|
||||
2. **System/Bank Level Entities (`isPersonalEntity = false`)**: userId may be optional or used for audit
|
||||
|
||||
Example from `MappedDynamicDataProvider.scala`:
|
||||
|
||||
```scala
|
||||
override def get(bankId: Option[String], entityName: String, id: String,
|
||||
userId: Option[String], isPersonalEntity: Boolean): Box[DynamicDataT] = {
|
||||
if (bankId.isEmpty && isPersonalEntity) {
|
||||
DynamicData.find(
|
||||
By(DynamicData.DynamicEntityName, entityName),
|
||||
By(DynamicData.DynamicDataId, id),
|
||||
By(DynamicData.UserId, userId.get) // <-- Filters by userId for personal entities
|
||||
) match {
|
||||
case Full(dynamicData) => Full(dynamicData)
|
||||
case _ => Failure(s"$DynamicDataNotFound dynamicEntityName=$entityName, dynamicDataId=$id, userId = $userId")
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Recommendation
|
||||
|
||||
For Dynamic Entities with consent support:
|
||||
|
||||
1. **Implement Option B**: Use `callContext.effectiveUserId` helper
|
||||
2. **Update all invocation points** in `APIMethodsDynamicEntity.scala` (lines 132, 213, 273, 280, 343, 351)
|
||||
3. **Document behavior**: Personal Dynamic Entities will be scoped to the consent user when consent is present
|
||||
4. **Security consideration**: Ensure consent validation happens before reaching Dynamic Entity operations
|
||||
|
||||
#### Files to Modify
|
||||
|
||||
- `OBP-API/obp-api/src/main/scala/code/api/dynamic/entity/APIMethodsDynamicEntity.scala` - Replace `Some(u.userId)` with `Some(callContext.effectiveUserId)`
|
||||
- `OBP-API/obp-api/src/main/scala/code/api/util/ApiSession.scala` - Add `effectiveUserId` helper to CallContext
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
### Key Files to Modify
|
||||
|
||||
- `OBP-API/obp-api/src/main/scala/code/api/util/ApiSession.scala` - CallContext and toOutboundAdapterCallContext
|
||||
- `OBP-API/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala` - Internal database operations
|
||||
|
||||
### Files to Review
|
||||
|
||||
- `OBP-API/obp-api/src/main/scala/code/api/util/ConsentUtil.scala` - Consent application logic
|
||||
- `OBP-API/obp-api/src/main/scala/code/bankconnectors/Connector.scala` - Base connector trait
|
||||
- `OBP-API/obp-api/src/main/scala/code/bankconnectors/rabbitmq/RabbitMQConnector_vOct2024.scala` - RabbitMQ example
|
||||
- `OBP-API/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala` - REST example
|
||||
- `OBP-API/obp-commons/src/main/scala/com/openbankproject/commons/model/CommonModel.scala` - DTO structures
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
To replace user_id at the connector level with consent user_id:
|
||||
|
||||
1. **Primary Solution (External Connectors)**: Modify `toOutboundAdapterCallContext` method in `ApiSession.scala` to use `consenter` when present instead of `user`
|
||||
|
||||
2. **Secondary Solution (LocalMappedConnector)**: Add helper methods like `effectiveUser` and `effectiveUserId` to `CallContext` and use them where user information is needed
|
||||
|
||||
3. **Best Practice**: Handle at API endpoint level where possible, before calling connector methods
|
||||
|
||||
4. **Key Insight**: The architectural difference between external connectors (which have a clear transformation boundary) and LocalMappedConnector (which works directly with domain objects) means there's no single universal solution
|
||||
|
||||
The `toOutboundAdapterCallContext` method is the ideal place for external connectors because it occurs:
|
||||
|
||||
- ✅ After security guards (authentication/authorization complete)
|
||||
- ✅ Before external communication (database/RabbitMQ/REST)
|
||||
- ✅ At a single transformation point (DRY principle)
|
||||
- ✅ With access to both authenticated user and consent user
|
||||
|
||||
---
|
||||
|
||||
_Last Updated: 2024_
|
||||
_OBP-API Version: v5.1.0+_
|
||||
Loading…
Reference in New Issue
Block a user