Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,56 @@ Adapters listen to message queues or remote calls, parse incoming messages accor
- Adapter in Go for high-performance transaction processing
- Adapter in Scala for Akka-based distributed systems

**Testing Adapters with Connector Endpoints:**

When building an adapter, you can use the `connector.name.export.as.endpoints` props setting to expose all of a connector's internal methods as REST endpoints. This is very useful during adapter development because it allows you to call individual connector methods directly (e.g. `getBank`, `getBankAccount`) and inspect their request/response payloads without needing to go through the full API layer.

When this property is set, OBP-API registers endpoints at `/obp/connector/{methodName}` which accept JSON request bodies matching the corresponding OutBound DTO and return JSON responses matching the InBound DTO. This lets you test each connector method in isolation.

```properties
# Export a connector's methods as REST endpoints for development/testing
# Set this to the connector name you are building an adapter for:
connector.name.export.as.endpoints=rabbitmq_vOct2024
```

**Validation rules:**
- If `connector=star`, the value must match one of the connectors listed in `starConnector_supported_types`
- If `connector=mapped`, the value can be `mapped`
- Otherwise, the value must match the `connector` props value (e.g. if `connector=rest_vMar2019`, set `connector.name.export.as.endpoints=rest_vMar2019`)

**Access control:** Calling these endpoints requires the `CanGetConnectorEndpoint` entitlement.

**Debugging Adapters with Connector Traces:**

Connector traces capture the full outbound (request) and inbound (response) messages for every connector call. This is invaluable when building an adapter because you can see exactly what OBP-API sent to your adapter and what it received back, making it easy to diagnose serialization issues, missing fields, or unexpected responses.

Enable connector traces with:

```properties
write_connector_trace=true
```

Each trace records:
- **correlationId** — links the trace to the originating API request
- **connectorName** — which connector was used (e.g. `rabbitmq_vOct2024`)
- **functionName** — the connector method called (e.g. `getBank`, `getBankAccount`)
- **bankId** — the bank identifier, if applicable
- **outboundMessage** — full serialized request parameters sent to the adapter
- **inboundMessage** — full serialized response received from the adapter
- **duration** — call duration in milliseconds
- **isSuccessful** — whether the call succeeded
- **userId**, **httpVerb**, **url** — context about the originating API request

Traces can be retrieved via the API:

```
GET /obp/v6.0.0/management/connector/traces
```

This endpoint supports filtering by `connector_name`, `function_name`, `correlation_id`, `bank_id`, `user_id`, `from_date`, `to_date`, and pagination with `limit` and `offset`. It requires the `CanGetConnectorTrace` entitlement.

There is also a **Connector Traces** page in **API Manager** which provides a UI for browsing and filtering connector traces.

---

### 3.13 Message Docs
Expand Down
11 changes: 11 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,17 @@ regulated_entities = []
# oidc_operator_initial_password=...
# oidc_operator_email=...

# Bootstrap OIDC Operator Consumer
# Given the following key and secret, OBP will create a consumer if it does not already exist.
# This consumer will be granted scopes: CanGetConsumers, CanCreateConsumer, CanVerifyOidcClient, CanGetOidcClient
# This allows OBP-OIDC to authenticate as an application and manage consumers via the API.
# Note: If you use this, you may not need the Bootstrap OIDC Operator User above,
# depending on how OBP-OIDC implements its authentication.
# If you want to use this feature, please set up both values properly at the same time.
# Both values must be between 10 and 250 characters.
# oidc_operator_consumer_key=...
# oidc_operator_consumer_secret=...


## Ethereum Connector Configuration
## ================================
Expand Down
72 changes: 71 additions & 1 deletion obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import code.cards.{MappedPhysicalCard, PinReset}
import code.connectormethod.ConnectorMethod
import code.consent.{ConsentRequest, MappedConsent}
import code.consumer.Consumers
import code.model.Consumer
import code.context.{MappedConsentAuthContext, MappedUserAuthContext, MappedUserAuthContextUpdate}
import code.counterpartylimit.CounterpartyLimit
import code.crm.MappedCrmEvent
Expand Down Expand Up @@ -121,8 +122,9 @@ import code.products.MappedProduct
import code.ratelimiting.RateLimiting
import code.regulatedentities.MappedRegulatedEntity
import code.regulatedentities.attribute.RegulatedEntityAttribute
import code.counterpartyattribute.{CounterpartyAttribute => CounterpartyAttributeMapper}
import code.scheduler._
import code.scope.{MappedScope, MappedUserScope}
import code.scope.{MappedScope, MappedUserScope, Scope}
import code.signingbaskets.{MappedSigningBasket, MappedSigningBasketConsent, MappedSigningBasketPayment}
import code.socialmedia.MappedSocialMedia
import code.standingorders.StandingOrder
Expand Down Expand Up @@ -337,6 +339,8 @@ class Boot extends MdcLoggable {

createBootstrapOidcOperatorUser()

createBootstrapOidcOperatorConsumer()

//launch the scheduler to clean the database from the expired tokens and nonces, 1 hour
DataBaseCleanerScheduler.start(intervalInSeconds = 60*60)

Expand Down Expand Up @@ -1095,6 +1099,71 @@ class Boot extends MdcLoggable {
}
}

/**
* Bootstrap OIDC Operator Consumer
* Given the following key and secret, OBP will create a consumer *if it does not exist already*.
* This consumer will be granted scopes: CanGetConsumers, CanCreateConsumer, CanVerifyOidcClient, CanGetOidcClient
* This allows OBP-OIDC to authenticate as an application (without a user) and manage consumers via the API.
*/
private def createBootstrapOidcOperatorConsumer() = {

val oidcOperatorConsumerKey = APIUtil.getPropsValue("oidc_operator_consumer_key", "")
val oidcOperatorConsumerSecret = APIUtil.getPropsValue("oidc_operator_consumer_secret", "")

val isPropsNotSetProperly = oidcOperatorConsumerKey == "" || oidcOperatorConsumerSecret == ""

if (isPropsNotSetProperly) {
logger.info(s"createBootstrapOidcOperatorConsumer says: oidc_operator_consumer_key and/or oidc_operator_consumer_secret props are not set, skipping")
} else if (oidcOperatorConsumerKey.length < 10) {
logger.error(s"createBootstrapOidcOperatorConsumer says: oidc_operator_consumer_key is too short (${oidcOperatorConsumerKey.length} chars, minimum 10), skipping")
} else if (oidcOperatorConsumerKey.length > 250) {
logger.error(s"createBootstrapOidcOperatorConsumer says: oidc_operator_consumer_key is too long (${oidcOperatorConsumerKey.length} chars, maximum 250), skipping")
} else if (oidcOperatorConsumerSecret.length < 10) {
logger.error(s"createBootstrapOidcOperatorConsumer says: oidc_operator_consumer_secret is too short (${oidcOperatorConsumerSecret.length} chars, minimum 10), skipping")
} else if (oidcOperatorConsumerSecret.length > 250) {
logger.error(s"createBootstrapOidcOperatorConsumer says: oidc_operator_consumer_secret is too long (${oidcOperatorConsumerSecret.length} chars, maximum 250), skipping")
} else {
val existingConsumer = Consumers.consumers.vend.getConsumerByConsumerKey(oidcOperatorConsumerKey)

if (existingConsumer.isDefined) {
logger.info(s"createBootstrapOidcOperatorConsumer says: Consumer with key ${oidcOperatorConsumerKey} already exists, skipping creation")
} else {
saveOidcOperatorConsumer(oidcOperatorConsumerKey, oidcOperatorConsumerSecret)
}
}
}

// Separate method to create and save the OIDC operator consumer.
// Uses Consumer.create directly (not Consumers.consumers.vend.createConsumer)
// to avoid S.? calls during Boot (Lift's S scope is not initialized at boot time).
private def saveOidcOperatorConsumer(consumerKey: String, consumerSecret: String): Unit = {
// Create consumer directly, skipping validate (which calls S.? and fails during Boot)
val c = Consumer.create
.key(consumerKey)
.secret(consumerSecret)
.name("OIDC Operator Consumer")
c.isActive(true) // MappedBoolean.apply returns Mapper, must be separate statement
c.description("Bootstrap consumer for OBP-OIDC to manage consumers via the API") // MappedText.apply returns Mapper, must be separate statement

val consumerBox = tryo(c.saveMe())

consumerBox match {
case Full(consumer) =>
logger.info(s"createBootstrapOidcOperatorConsumer says: Consumer created successfully with consumer_id: ${consumer.consumerId.get}")
val scopes = List(CanGetConsumers, CanCreateConsumer, CanVerifyOidcClient, CanGetOidcClient)
scopes.foreach { role =>
val resultBox = Scope.scope.vend.addScope("", consumer.id.get.toString, role.toString)
if (resultBox.isEmpty) {
logger.error(s"createBootstrapOidcOperatorConsumer says: Error granting scope ${role}: ${resultBox}")
}
}
case net.liftweb.common.Failure(msg, exception, _) =>
logger.error(s"createBootstrapOidcOperatorConsumer says: Error creating consumer: $msg ${exception.map(_.getMessage).openOr("")}")
case _ =>
logger.error("createBootstrapOidcOperatorConsumer says: Error creating consumer (unknown error)")
}
}

LiftRules.statelessDispatch.append(aliveCheck)

}
Expand Down Expand Up @@ -1225,6 +1294,7 @@ object ToSchemify {
CustomerAccountLink,
TransactionIdMapping,
RegulatedEntityAttribute,
CounterpartyAttributeMapper,
BankAccountBalance,
Group,
AccountAccessRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6080,7 +6080,27 @@ object SwaggerDefinitionsJSON {
lazy val regulatedEntityAttributesJsonV510 = RegulatedEntityAttributesJsonV510(
List(regulatedEntityAttributeResponseJsonV510)
)


lazy val counterpartyAttributeRequestJsonV600 = CounterpartyAttributeRequestJsonV600(
name = "TAX_NUMBER",
attribute_type = "STRING",
value = "123456789",
is_active = Some(true)
)

lazy val counterpartyAttributeResponseJsonV600 = CounterpartyAttributeResponseJsonV600(
counterparty_id = counterpartyIdExample.value,
counterparty_attribute_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f",
name = "TAX_NUMBER",
attribute_type = "STRING",
value = "123456789",
is_active = Some(true)
)

lazy val counterpartyAttributesJsonV600 = CounterpartyAttributesJsonV600(
List(counterpartyAttributeResponseJsonV600)
)

lazy val bankAccountBalanceRequestJsonV510 = BankAccountBalanceRequestJsonV510(
balance_type = balanceTypeExample.value,
balance_amount = balanceAmountExample.value
Expand Down
8 changes: 8 additions & 0 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,14 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
if (authMode == ApplicationOnly) {
errorResponseBodies ?-= AuthenticatedUserIsRequired
}
if (authMode == UserOrApplication) {
description +=
s"""
|
|This endpoint supports **User OR Application** authentication. You can authenticate either as a logged-in User (with Entitlements) or as an Application using a Consumer Key (with Scopes).
|See ${Glossary.getGlossaryItemLink("API.Endpoint Auth Modes")} for more information.
|"""
}
case UserAndApplication =>
errorResponseBodies ?+= AuthenticatedUserIsRequired
errorResponseBodies ?+= ApplicationNotIdentified
Expand Down
15 changes: 15 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiRole.scala
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,21 @@ object ApiRole extends MdcLoggable{
case class CanDeleteRegulatedEntityAttribute(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteRegulatedEntityAttribute = CanDeleteRegulatedEntityAttribute()

case class CanGetCounterpartyAttribute(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetCounterpartyAttribute = CanGetCounterpartyAttribute()

case class CanGetCounterpartyAttributes(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetCounterpartyAttributes = CanGetCounterpartyAttributes()

case class CanCreateCounterpartyAttribute(requiresBankId: Boolean = false) extends ApiRole
lazy val canCreateCounterpartyAttribute = CanCreateCounterpartyAttribute()

case class CanUpdateCounterpartyAttribute(requiresBankId: Boolean = false) extends ApiRole
lazy val canUpdateCounterpartyAttribute = CanUpdateCounterpartyAttribute()

case class CanDeleteCounterpartyAttribute(requiresBankId: Boolean = false) extends ApiRole
lazy val canDeleteCounterpartyAttribute = CanDeleteCounterpartyAttribute()


case class CanGetMethodRoutings(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetMethodRoutings = CanGetMethodRoutings()
Expand Down
1 change: 1 addition & 0 deletions obp-api/src/main/scala/code/api/util/ApiTag.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ object ApiTag {
val apiTagScope = ResourceDocTag("Scope")
val apiTagOwnerRequired = ResourceDocTag("OwnerViewRequired")
val apiTagCounterparty = ResourceDocTag("Counterparty")
val apiTagCounterpartyAttribute = ResourceDocTag("Counterparty-Attribute")
val apiTagKyc = ResourceDocTag("KYC")
val apiTagCustomer = ResourceDocTag("Customer")
val apiTagRetailCustomer = ResourceDocTag("Retail-Customer")
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/util/DoobieQueries.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object DoobieQueries {
*/
def getDistinctProviders: List[String] = {
val query: ConnectionIO[List[String]] =
sql"""SELECT DISTINCT provider_ FROM resourceuser ORDER BY provider_"""
sql"""SELECT DISTINCT provider_ FROM resourceuser WHERE provider_ IS NOT NULL ORDER BY provider_"""
.query[String]
.to[List]

Expand Down
Loading
Loading