Skip to content
Open
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
61 changes: 38 additions & 23 deletions ideas/ASSET_REGISTRY.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ The Open Bank Project (OBP) is an open-source RESTful API platform for banks tha
- **Views System**: Account data visibility control (owner, public, accountant, auditor, custom)
- **Scope Management**: OAuth 2.0 scope definitions
- **Authentication Type Validation**: Enforce authentication requirements per endpoint
- **Endpoint Auth Modes**: Per-endpoint control over user vs application authentication (UserOnly, ApplicationOnly, UserOrApplication, UserAndApplication)
- **Endpoint Auth Modes**: Per-endpoint control over user vs application authentication (UserOnly, ApplicationOnly, UserXORApplication, UserAndApplication)
- **Mandates**: Corporate account governance with provisions, signatory panels, and multi-signature approval

#### 1.2.9 Extensibility & Customization
Expand Down Expand Up @@ -2699,7 +2699,7 @@ OBP can bootstrap a Consumer (Application) for OBP-OIDC at startup. This allows

The bootstrap consumer is granted the following Scopes: `CanGetConsumers`, `CanCreateConsumer`, `CanVerifyOidcClient`, `CanGetOidcClient`, `CanGetOidcConsent`.

These endpoints use `authMode = UserOrApplication`, meaning they can be accessed either by a logged-in User with Entitlements, or by an Application using a Consumer Key with Scopes.
These endpoints use `authMode = UserXORApplication`, meaning they can be accessed either by a logged-in User with Entitlements, or by the Application acting as itself (e.g. with a client-credentials token) with its Consumer's Scopes. A User calling through the Consumer does not get its Scopes.

```properties
# Bootstrap OIDC Operator Consumer
Expand Down Expand Up @@ -3022,18 +3022,18 @@ Previously, a global `allow_entitlements_or_scopes` config flag allowed consumer
|------|-------------|
| `UserOnly` | (Default) Requires user authentication and user entitlements |
| `ApplicationOnly` | Requires consumer scope only, no user authentication needed |
| `UserOrApplication` | Either consumer scope OR user entitlement is sufficient |
| `UserXORApplication` | Consumer scope when the application acts as itself (no user, or its own client-credentials user, provider `obp-oidc#client`); user entitlement when a person acts. A person never uses the consumer's scopes. Formerly `UserOrApplication` (either was sufficient) |
| `UserAndApplication` | Both consumer scope AND user entitlement are required |

**Migration from `allow_entitlements_or_scopes`:**

The global `allow_entitlements_or_scopes` config flag has been removed. Deployments that previously used `allow_entitlements_or_scopes=true` must now set `authMode = UserOrApplication` on individual endpoint `ResourceDoc` instances.
The global `allow_entitlements_or_scopes` config flag has been removed. Deployments that previously used `allow_entitlements_or_scopes=true` must now set `authMode = UserXORApplication` on individual endpoint `ResourceDoc` instances.

The global overrides `require_scopes_for_all_roles` and `require_scopes_for_listed_roles` continue to work and force `UserAndApplication` behavior.
The global overrides `require_scopes_for_all_roles` and `require_scopes_for_listed_roles` continue to work and force `UserAndApplication` behavior, except for an application acting as itself on an `ApplicationOnly` or `UserXORApplication` endpoint, which still needs only its scopes.

**Example:**

The `verifyUserCredentials` endpoint uses `UserOrApplication` mode, so it can be called either by a logged-in user with the appropriate entitlement, or by an application with the appropriate consumer scope.
The `verifyUserCredentials` endpoint uses `UserXORApplication` mode, so it can be called either by a logged-in user with the appropriate entitlement, or by an application acting as itself with the appropriate consumer scope.

### 8.8 Maker/Checker for Transaction Requests

Expand Down Expand Up @@ -4298,7 +4298,7 @@ docker run -p 8080:8080 \

**Dynamic Endpoint:** User-defined API endpoint

**Endpoint Auth Mode:** Per-endpoint setting controlling whether user authentication, application scope, or both are required (UserOnly, ApplicationOnly, UserOrApplication, UserAndApplication)
**Endpoint Auth Mode:** Per-endpoint setting controlling whether user authentication, application scope, or both are required (UserOnly, ApplicationOnly, UserXORApplication, UserAndApplication)

**Entitlement:** Permission to perform specific operation (same as Role)

Expand Down
12 changes: 11 additions & 1 deletion obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,13 @@ jwt.use.ssl=false
## Unit is a second
# obp_expired_consents_interval_in_seconds =

## Reject OBP consents still "INITIATED" (their challenge was never answered) after a defined time.
## They never granted access; rejecting them stops an app that reuses a pending consent from being stuck on one.
## Both have defaults: the task runs every 593 seconds, and rejects consents unchanged for 21600 seconds (6 hours).
## Set the interval to 0 to stop the task.
# obp_unfinished_consents_time_in_seconds = 21600
# obp_unfinished_consents_interval_in_seconds = 593

## Expire UK Open Banking consents with status "Authorised"
## Unlike the three keys above this one HAS a default (601 seconds, a prime near 10 minutes, so the
## sweep does not settle into step with other periodic schedulers). Set it to 0 to stop the task.
Expand Down Expand Up @@ -914,6 +921,8 @@ defaultBank.bank_id=OBP
## CanCreateEntitlementAtAnyBank
## CanCreateEntitlementAtOneBank
## CanGetAnyUser
## CanGetEntitlementRequestsAtAnyBank
## CanDeleteEntitlementRequestsAtAnyBank
## After they have granted these roles, they can grant further roles and remove their
# user_id from the super_admin_user_ids list because its redundant.
## Once you have the roles above you can grant any other system or bank related roles to yourself.
Expand Down Expand Up @@ -1436,7 +1445,8 @@ database_messages_scheduler_interval=3600
# ---------------------------------------------------

# -- Consents ---------------------------------------------
# In case isn't defined default value is "false"
# In case isn't defined default value is "false". While false, no consent is accepted as a login (OBP-35007)
# and none is created either: every endpoint that creates a consent or a consent request answers OBP-35007.
# consents.allowed=true
#
# In order to pin a consent to a consumer we can use the property consumer_validation_method_for_consent
Expand Down
6 changes: 4 additions & 2 deletions obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ import code.etag.MappedETag
import code.featuredapicollection.FeaturedApiCollection
import code.fx.{MappedCurrency, MappedFXRate}
import code.group.Group
import code.organisation.Organisation
import code.tenant.Tenant
import code.routingscheme.{RoutingScheme, BankSupportedRoutingScheme}
import code.payeelookup.PayeeLookup
import code.utilitypayment.UtilityPaymentCallback
Expand Down Expand Up @@ -1159,7 +1159,8 @@ object ToSchemify extends MdcLoggable {
BankAccountBalance,
Group,
code.group.GroupMembership,
Organisation,
Tenant,
code.model.dataAccess.SelfServiceBank,
RoutingScheme,
BankSupportedRoutingScheme,
code.asset.Asset,
Expand All @@ -1169,6 +1170,7 @@ object ToSchemify extends MdcLoggable {
code.platformapp.PlatformAppRequiredScope,
code.domainapi.DomainApi,
code.dynamicentitywebhook.DynamicEntityWebhook,
code.accountcreation.AccountCreation,
PayeeLookup,
UtilityPaymentCallback,
BulkPayment,
Expand Down
134 changes: 134 additions & 0 deletions obp-api/src/main/scala/code/accountcreation/AccountCreation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
Open Bank Project - API
Copyright (C) 2011-2026, TESOBE GmbH.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

Email: contact@tesobe.com
TESOBE GmbH.
Osloer Strasse 16/17
Berlin 13359, Germany

This product includes software developed at
TESOBE (http://www.tesobe.com/)

*/

package code.accountcreation

import java.util.Date

import code.consent.Consents
import code.users.{UserReference, Users}
import net.liftweb.common.{Box, Full}
import net.liftweb.mapper._
import net.liftweb.util.Helpers.tryo
import net.liftweb.util.SimpleInjector

/**
* Who created a bank account, and for whom.
*
* The account itself is created through the Connector, which may keep it outside OBP's database, so
* the creator cannot be a column of the account. This table holds one row per account created
* through v7.0.0 Create Account. It records both ids, like a Counterparty or a Dynamic Entity
* webhook: the caller, which is an agent's own id when an agent made the call, and the person the
* call was made for. See UserReference.AccountCreation_CreatedByUserId. When an agent made the call,
* it also records the consent_reference_id of the Consent it acted under, the id the metrics record
* for the same call.
*/
object AccountCreations extends SimpleInjector {
val accountCreationProvider = new Inject(() => buildOne) {}
def buildOne: AccountCreationProvider = AccountCreationDbProvider
}

trait AccountCreationTrait {
def bankId: String
def accountId: String
def createdByUserId: String
def createdByOnBehalfOfUserId: String
/** The Consent the agent acted under; None when the caller acted for themselves. */
def consentReferenceId: Option[String]
def createdAt: Date
}

trait AccountCreationProvider {
/** Records that `createdByUserId`, the authenticated caller, created the account. */
def record(bankId: String, accountId: String, createdByUserId: String): Box[AccountCreationTrait]
def get(bankId: String, accountId: String): Box[AccountCreationTrait]
/** The records of those of `accountIds` at `bankId` that have one, in one query. */
def getForAccounts(bankId: String, accountIds: List[String]): List[AccountCreationTrait]
}

object AccountCreationDbProvider extends AccountCreationProvider {

override def record(bankId: String, accountId: String, createdByUserId: String): Box[AccountCreationTrait] =
for {
attribution <- Users.users.vend.attributionOf(createdByUserId, UserReference.AccountCreation_CreatedByUserId)
consentReferenceId = attribution.consentId
.flatMap(id => Consents.consentProvider.vend.getConsentByConsentId(id).toOption)
.map(_.consentReferenceId)
row <- tryo {
AccountCreation.create
.BankId(bankId)
.AccountId(accountId)
.CreatedByUserId(attribution.userId)
.CreatedByOnBehalfOfUserId(attribution.onBehalfOfUserId)
.ConsentReferenceId(consentReferenceId.orNull)
.saveMe()
}
} yield row

override def get(bankId: String, accountId: String): Box[AccountCreationTrait] =
AccountCreation.find(By(AccountCreation.BankId, bankId), By(AccountCreation.AccountId, accountId))

override def getForAccounts(bankId: String, accountIds: List[String]): List[AccountCreationTrait] =
if (accountIds.isEmpty) Nil
else AccountCreation.findAll(By(AccountCreation.BankId, bankId), ByList(AccountCreation.AccountId, accountIds))
}

class AccountCreation extends AccountCreationTrait with LongKeyedMapper[AccountCreation] with IdPK {
def getSingleton = AccountCreation

object BankId extends MappedString(this, 255) {
override def dbColumnName = "bank_id"
}
object AccountId extends MappedString(this, 255) {
override def dbColumnName = "account_id"
}
object CreatedByUserId extends MappedString(this, 255) {
override def dbColumnName = "created_by_user_id"
}
object CreatedByOnBehalfOfUserId extends MappedString(this, 255) {
override def dbColumnName = "created_by_on_behalf_of_user_id"
}
object ConsentReferenceId extends MappedString(this, 255) {
override def dbColumnName = "consent_reference_id"
}
object CreatedAt extends MappedDateTime(this) {
override def dbColumnName = "created_at"
override def defaultValue = new Date()
}

override def bankId: String = BankId.get
override def accountId: String = AccountId.get
override def createdByUserId: String = CreatedByUserId.get
override def createdByOnBehalfOfUserId: String = CreatedByOnBehalfOfUserId.get
override def consentReferenceId: Option[String] = Option(ConsentReferenceId.get).filter(_.nonEmpty)
override def createdAt: Date = CreatedAt.get
}

object AccountCreation extends AccountCreation with LongKeyedMetaMapper[AccountCreation] {
override def dbTableName = "account_creation"
override def dbIndexes = UniqueIndex(BankId, AccountId) :: super.dbIndexes
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ object OpenAPI31JSONFactory extends MdcLoggable {
|
|### Client Credentials Flow (2-legged / Application Only)
|
|Use this flow for endpoints with authMode ApplicationOnly or UserOrApplication.
|Use this flow for endpoints with authMode ApplicationOnly or UserXORApplication.
|The application authenticates itself using its client_id and client_secret without a user login.
|
| POST $oidcTokenEndpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import cats.data.{Kleisli, OptionT}
import cats.effect.IO
import code.api.Constant
import code.api.UKOpenBanking.v3_1_0.JSONFactory_UKOpenBanking_310.ConsentPostBodyUKV310
import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, UserOrApplication, connectorEmptyResponse, mockedDataText, passesPsd2Aisp, unboxFullOrFail, parseIso8601OrDayDate}
import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, UserXORApplication, connectorEmptyResponse, mockedDataText, passesPsd2Aisp, unboxFullOrFail, parseIso8601OrDayDate}
import code.api.util.ApiTag
import code.api.util.CustomJsonFormats
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, ConsentNotFound, ConsentViewNotFund, InvalidJsonFormat, InvalidUKConsentPermissions, UnknownError}
Expand Down Expand Up @@ -75,6 +75,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
EndpointHelpers.executeFutureCreated(req) {
implicit val cc: CallContext = req.callContext
for {
_ <- code.api.util.Consent.checkConsentsAllowed(Some(cc))
// Client-credentials lodging: require some authentication (consumer or user) but not a
// PSU specifically; reject only a fully anonymous request. The PSU is bound later at
// authorise time. Mirrors the Berlin Group native consent flow and the v4.0.1 handler.
Expand Down Expand Up @@ -201,8 +202,8 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
// Without it the doc defaults to UserOnly, which sends the middleware down anonymousAccess and
// 401s any request that carries no user -- so the endpoint only works today because OAuth2
// token parsing auto-vivifies a user for a client-credentials token. Matches the Berlin Group
// twin (Http4sBGv13AIS.createConsent), which has always been UserOrApplication.
authMode = UserOrApplication,
// twin (Http4sBGv13AIS.createConsent), which has always been UserXORApplication.
authMode = UserXORApplication,
http4sPartialFunction = Some(createAccountAccessConsents)
)

Expand Down Expand Up @@ -239,7 +240,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
ApiTag("Account Access") :: Nil,
// As with the POST that lodges the consent: revoking is a client-credentials call in the
// standard's AISP flow, so a PSU cannot be required. See Consent.checkUKConsentAccess.
authMode = UserOrApplication,
authMode = UserXORApplication,
http4sPartialFunction = Some(deleteAccountAccessConsentsConsentId)
)

Expand Down Expand Up @@ -317,7 +318,7 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
List(AuthenticatedUserIsRequired, UnknownError),
ApiTag("Account Access") :: Nil,
// As above: the AISP polls its own consent with a client-credentials token.
authMode = UserOrApplication,
authMode = UserXORApplication,
http4sPartialFunction = Some(getAccountAccessConsentsConsentId)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import cats.effect.IO
import code.api.APIFailureNewStyle
import code.api.Constant
import code.api.UKOpenBanking.v3_1_0.JSONFactory_UKOpenBanking_310.ConsentPostBodyUKV310
import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, HTTPParam, UserOrApplication, connectorEmptyResponse, createQueriesByHttpParams, defaultBankId, fullBoxOrException, passesPsd2Aisp, unboxFull, unboxFullOrFail, parseIso8601OrDayDate}
import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, EmptyBody, ResourceDoc, HTTPParam, UserXORApplication, connectorEmptyResponse, createQueriesByHttpParams, defaultBankId, fullBoxOrException, passesPsd2Aisp, unboxFull, unboxFullOrFail, parseIso8601OrDayDate}
import code.api.util.ApiTag
import code.api.util.CallContext
import code.api.util.CustomJsonFormats
Expand Down Expand Up @@ -124,6 +124,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
EndpointHelpers.executeFutureCreated(req) {
implicit val cc: CallContext = req.callContext
for {
_ <- code.api.util.Consent.checkConsentsAllowed(Some(cc))
// Spec Step 2: the TPP lodges the consent via a client-credentials grant -- authenticated
// as an app (consumer) but with no PSU yet. Require some authentication (a consumer or a
// user) but not a PSU specifically; reject only a fully anonymous request. The PSU is
Expand Down Expand Up @@ -209,8 +210,8 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
// Without it the doc defaults to UserOnly, which sends the middleware down anonymousAccess and
// 401s any request that carries no user -- so the endpoint only works today because OAuth2
// token parsing auto-vivifies a user for a client-credentials token. Matches the Berlin Group
// twin (Http4sBGv13AIS.createConsent), which has always been UserOrApplication.
authMode = UserOrApplication,
// twin (Http4sBGv13AIS.createConsent), which has always been UserXORApplication.
authMode = UserXORApplication,
http4sPartialFunction = Some(createAccountAccessConsents)
)

Expand Down Expand Up @@ -290,7 +291,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
ApiTag("Account Access Consents") :: Nil,
// Same reasoning as the POST that lodges the consent: the AISP polls it with a
// client-credentials token, so a PSU cannot be required. See Consent.checkUKConsentAccess.
authMode = UserOrApplication,
authMode = UserXORApplication,
http4sPartialFunction = Some(getAccountAccessConsentsConsentId)
)

Expand Down Expand Up @@ -323,7 +324,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
List(AuthenticatedUserIsRequired, UnknownError),
ApiTag("Account Access Consents") :: Nil,
// As above: revoking is a client-credentials call in the standard's AISP flow.
authMode = UserOrApplication,
authMode = UserXORApplication,
http4sPartialFunction = Some(deleteAccountAccessConsentsConsentId)
)

Expand Down
Loading
Loading