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
430 changes: 430 additions & 0 deletions ON_BEHALF_OF_USER_ID_PLAN.md

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -946,8 +946,17 @@ If it is required for a "STABLE" api to be changed, then the class metadata must

### Steps to freeze an API

- Run the FrozenClassUtil to regenerate persist file of frozen apis information, the file is `PROJECT_ROOT_PATH/obp-api/src/test/resources/frozen_type_meta_data`
- push the file `frozen_type_meta_data` to github
- Set the version's `versionStatus` to `ApiVersionStatus.STABLE` in both places it is defined (`OBPAPIx_y_z` and `Http4sXYZ`).
- Regenerate the snapshot and its text rendering from Maven, on the reactor classpath:

```sh
FROZEN_REGENERATE=true mvn -pl obp-api -am test -DwildcardSuites=code.util.FrozenSnapshotGenerate
```

Without `FROZEN_REGENERATE=true` the suite cancels, so a normal test run never rewrites the snapshot.
- Review `git diff obp-api/src/test/resources/` (the `.txt` shows the endpoints and fields that entered or left the freeze), run `code.util.FrozenClassTest` and `code.util.FrozenMetaDataTextTest`, then commit `frozen_type_meta_data` together with `frozen_type_meta_data.txt`.

Running the generator as a plain JVM main (`code.util.FrozenClassUtil`, then `code.util.FrozenMetaDataText`) still works but needs care: put `obp-api/target/test-classes` first on the classpath (the output path is derived from the first classpath root, or set `-Dfrozen.metadata.path=...`), and build the classpath from the reactor rather than `dependency:build-classpath`, which resolves `obp-commons` from `~/.m2` and can drag in a stale build with Scala 2.12 dependencies. Reinstall `obp-commons` (`mvn install -pl obp-commons -DskipTests`) whenever it changes.

There is a video about the detail: [demonstrate the detail of the feature](https://www.youtube.com/watch?v=m9iYCSM0bKA)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4313,8 +4313,6 @@ docker run -p 8080:8080 \

**Consent:** Permission granted by user for data access

**isNaturalPerson:** Boolean field on User that distinguishes human users (true, default) from service accounts/machine users (false)

**Mandate:** Formal agreement between a corporate customer and a bank defining who can operate an account, what they can do, and under what conditions

**Direct Login:** Username/password authentication method
Expand All @@ -4331,7 +4329,7 @@ docker run -p 8080:8080 \

**Opey:** AI-powered banking assistant

**principalUserId:** Optional field on User that links a service/agent user back to the human principal it acts on behalf of, formalising the Human Agent delegation chain
**on_behalf_of_user_id:** The user a request is made on behalf of: for a consent user (a User row created by a Consent, `created_by_consent_id` set) it is the Consent's user; for any other user it is the user itself. Not a stored column on User — resolved from the Consent at read time. Distinct from `user_id`, which is always the authenticated caller

**Props:** Configuration properties file

Expand Down
10 changes: 7 additions & 3 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ public_keycloak_url=http://localhost:7787
public_obp_hola_url=http://localhost:8087
public_obp_mcp_url=http://localhost:9100
public_obp_opey_url=http://localhost:5000
# OBP-Stripe subscription / payment service (Go server, default port 4242)
public_obp_stripe_url=http://localhost:4242

## This port is used for local development
## Note: OBP-API now uses http4s server
Expand Down Expand Up @@ -1158,12 +1160,14 @@ featured_apis=elasticSearchWarehouseV300
# Default is now true. This property may be removed in a future version.
# Set to false to use only system-wide defaults (not recommended)
# use_consumer_limits=true
# In case isn't defined default value is 60
# user_consumer_limit_anonymous_access=100
# Per-hour limit for anonymous calls (no consumer). Default 1000. 0 blocks all anonymous access, -1 removes the limit.
# user_consumer_limit_anonymous_access=1000
# For the Rate Limiting feature we use Redis cache instance
# In case isn't defined default value is root
# rate_limiting.exclude_endpoints=root
## Default rate limiting for a new consumer
## Default rate limits for a consumer that has no rate limit records at all.
## -1 = unlimited, 0 = blocked, positive = max calls in the period.
## Once a consumer has any record, the record's values apply instead (its -1 is a literal unlimited).
# rate_limiting_per_second = -1
# rate_limiting_per_minute = -1
# rate_limiting_per_hour = -1
Expand Down
5 changes: 5 additions & 0 deletions obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import code.apicollection.ApiCollection
import code.apicollectionendpoint.ApiCollectionEndpoint
import code.apiproduct.ApiProduct
import code.apiproductattribute.ApiProductAttribute
import code.apiproductsubscription.{ApiProductSubscription, ApiProductSubscriptionScope}
import code.apiproductsubscriptionattribute.ApiProductSubscriptionAttribute
import code.atmattribute.AtmAttribute
import code.atms.MappedAtm
import code.authtypevalidation.AuthenticationTypeValidation
Expand Down Expand Up @@ -996,6 +998,9 @@ object ToSchemify extends MdcLoggable {
ApiCollectionEndpoint,
ApiProduct,
ApiProductAttribute,
ApiProductSubscription,
ApiProductSubscriptionScope,
ApiProductSubscriptionAttribute,
FeaturedApiCollection,
JsonSchemaValidation,
AuthenticationTypeValidation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ trait AccountHolders {
* @return
*/
def getAccountsHeldByUser(user: User, source: Option[String] = None): Set[BankIdAccountId]
/** Links the account to its holder. The holder is the on-behalf-of user of `user`
* (UserReference.AccountHolderUser): a consent user never holds an account, the user its
* consent names does. Same user for an original user. */
def getOrCreateAccountHolder(user: User, bankAccountUID :BankIdAccountId, source: Option[String] = None): Box[MapperAccountHolders] //There is no AccountHolder trait, database structure different with view
def deleteAccountHolder(user: User, bankAccountUID :BankIdAccountId): Box[Boolean]
def bulkDeleteAllAccountHolders(): Box[Boolean]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,26 @@ object MapperAccountHolders extends MapperAccountHolders with AccountHolders wit

//Note, this method, will not check the existing of bankAccount, any value of BankIdAccountId
//Can create the MapperAccountHolders.
def getOrCreateAccountHolder(user: User, bankIdAccountId :BankIdAccountId, source: Option[String] = None): Box[MapperAccountHolders] ={
//
// On-behalf-of guard (attribution policy UserReference.AccountHolderUser): an account is
// held by the on-behalf-of user. When `user` is a consent user the holder row is written
// for the user the consent names, so the account does not strand when the consent dies.
// For an original user this is a no-op. The resolver logs every redirect.
// ON_BEHALF_OF_USER_ID_PLAN.md, Phase 2.
def getOrCreateAccountHolder(user: User, bankIdAccountId :BankIdAccountId, source: Option[String] = None): Box[MapperAccountHolders] =
for {
holder <- accountHolderUserFor(user)
accountHolder <- getOrCreateAccountHolderRow(holder, bankIdAccountId, source)
} yield accountHolder

/** The user the holder row is written for: `user` itself, or its on-behalf-of user. */
private def accountHolderUserFor(user: User): Box[User] =
Users.users.vend.attributedUserId(user.userId, code.users.UserReference.AccountHolderUser).flatMap { holderUserId =>
if (holderUserId == user.userId) Full(user)
else Users.users.vend.getUserByUserId(holderUserId) ?~ s"getOrCreateAccountHolder: on-behalf-of user $holderUserId of ${user.userId} not found"
}

private def getOrCreateAccountHolderRow(user: User, bankIdAccountId :BankIdAccountId, source: Option[String]): Box[MapperAccountHolders] ={

val mapperAccountHolder = MapperAccountHolders.find(
By(MapperAccountHolders.user, user.userPrimaryKey.value),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4696,6 +4696,17 @@ object SwaggerDefinitionsJSON {
time_to_live = Some(3600)
)
lazy val postConsentRequestJsonV310 = postConsentPhoneJsonV310.copy(consumer_id = None)
lazy val postConsentBodyJsonV600 = code.api.v6_0_0.PostConsentBodyJsonV600(
everything = false,
bank_id = None,
views = List(PostConsentViewJsonV310(bankIdExample.value, accountIdExample.value, viewIdExample.value)),
entitlements = List(PostConsentEntitlementJsonV310(bankIdExample.value, "CanGetCustomersAtOneBank")),
consumer_id = Some(consumerIdExample.value),
consent_request_id = None,
valid_from = Some(new Date()),
time_to_live = Some(3600),
my_resources = Some(code.api.v6_0_0.PostConsentMyResourcesJson(Some(List(code.api.v6_0_0.PostConsentPersonalDynamicEntityJson("", "FooBar", List("read", "write"))))))
)

lazy val consentsJsonV310 = ConsentsJsonV310(List(consentJsonV310))

Expand Down Expand Up @@ -5392,6 +5403,7 @@ object SwaggerDefinitionsJSON {
)
lazy val apiProductsJsonV600 = ApiProductsJsonV600(List(apiProductJsonV600))


lazy val productJsonV600 = ProductJsonV600(
bank_id = bankIdExample.value,
product_code = productCodeExample.value,
Expand Down Expand Up @@ -6348,7 +6360,8 @@ object SwaggerDefinitionsJSON {
ConfigPropJsonV600("public_keycloak_url", "http://localhost:7787"),
ConfigPropJsonV600("public_obp_hola_url", "http://localhost:48123"),
ConfigPropJsonV600("public_obp_mcp_url", "http://localhost:9100"),
ConfigPropJsonV600("public_obp_opey_url", "http://localhost:5000")
ConfigPropJsonV600("public_obp_opey_url", "http://localhost:5000"),
ConfigPropJsonV600("public_obp_stripe_url", "http://localhost:4242")
)
)

Expand Down
9 changes: 7 additions & 2 deletions obp-api/src/main/scala/code/api/cache/Caching.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ object Caching extends MdcLoggable {
* @return Number of cache keys deleted
*/
def invalidateRateLimitCache(consumerId: String): Int = {
val pattern = s"${RATE_LIMIT_ACTIVE_PREFIX}${consumerId}_*"
// scalacache stores the entry as
// <serialization namespace>:code.api.cache.Redis.memoizeSyncWithRedis(Some(<our cache key>))()()
// so the glob must be unanchored at the front, as "*getMethodRoutings*" is. Without the
// leading "*" this deleted nothing (silently) and a new or changed rate limit only took
// effect when the hour cache expired. Pinned by CacheKeyFormatTest.
val pattern = s"*${RATE_LIMIT_ACTIVE_PREFIX}${consumerId}_*"
Redis.deleteKeysByPattern(pattern)
}

Expand All @@ -128,7 +133,7 @@ object Caching extends MdcLoggable {
* @return Number of cache keys deleted
*/
def invalidateAllRateLimitCache(): Int = {
val pattern = s"${RATE_LIMIT_ACTIVE_PREFIX}*"
val pattern = s"*${RATE_LIMIT_ACTIVE_PREFIX}*"
Redis.deleteKeysByPattern(pattern)
}

Expand Down
Loading
Loading