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

Large diffs are not rendered by default.

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
8 changes: 5 additions & 3 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -1158,12 +1158,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 @@ -5392,6 +5392,7 @@ object SwaggerDefinitionsJSON {
)
lazy val apiProductsJsonV600 = ApiProductsJsonV600(List(apiProductJsonV600))


lazy val productJsonV600 = ProductJsonV600(
bank_id = bankIdExample.value,
product_code = productCodeExample.value,
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ object DynamicEntityHelper {
private val implementedInApiVersion = ApiVersion.v4_0_0

// (Some(BankId), EntityName, DynamicEntityInfo)
def definitionsMap: Map[(Option[String], String), DynamicEntityInfo] = NewStyle.function.getDynamicEntities(None, true).map(it => ((it.bankId, it.entityName), DynamicEntityInfo(it.metadataJson, it.entityName, it.bankId, it.hasPersonalEntity, it.hasPublicAccess, it.hasCommunityAccess, it.personalRequiresRole, it.useRowLevelAccess))).toMap
def definitionsMap: Map[(Option[String], String), DynamicEntityInfo] = NewStyle.function.getDynamicEntities(None, true).map(it => ((it.bankId, it.entityName), DynamicEntityInfo(it.metadataJson, it.entityName, it.bankId, it.hasPersonalEntity, it.hasPublicAccess, it.hasCommunityAccess, it.personalRequiresRole, it.useRowLevelAccess, it.authMode))).toMap

def dynamicEntityRoles: List[String] = NewStyle.function.getDynamicEntities(None, true).flatMap { dEntity =>
val baseRoles = DynamicEntityInfo.roleNames(dEntity.entityName, dEntity.bankId)
Expand Down Expand Up @@ -274,7 +274,8 @@ object DynamicEntityHelper {
),
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
Some(List(dynamicEntityInfo.canGetRole)),
createdByBankId= dynamicEntityInfo.bankId
createdByBankId= dynamicEntityInfo.bankId,
authMode = dynamicEntityInfo.endpointAuthMode
)

resourceDocs += (DynamicEntityOperation.GET_ONE, splitNameWithBankId) -> ResourceDoc(
Expand All @@ -301,7 +302,8 @@ object DynamicEntityHelper {
),
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
Some(List(dynamicEntityInfo.canGetRole)),
createdByBankId= dynamicEntityInfo.bankId
createdByBankId= dynamicEntityInfo.bankId,
authMode = dynamicEntityInfo.endpointAuthMode
)

resourceDocs += (DynamicEntityOperation.CREATE, splitNameWithBankId) -> ResourceDoc(
Expand Down Expand Up @@ -330,7 +332,8 @@ object DynamicEntityHelper {
),
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
Some(List(dynamicEntityInfo.canCreateRole)),
createdByBankId= dynamicEntityInfo.bankId
createdByBankId= dynamicEntityInfo.bankId,
authMode = dynamicEntityInfo.endpointAuthMode
)

resourceDocs += (DynamicEntityOperation.UPDATE, splitNameWithBankId) -> ResourceDoc(
Expand Down Expand Up @@ -359,7 +362,8 @@ object DynamicEntityHelper {
),
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
Some(List(dynamicEntityInfo.canUpdateRole)),
createdByBankId= dynamicEntityInfo.bankId
createdByBankId= dynamicEntityInfo.bankId,
authMode = dynamicEntityInfo.endpointAuthMode
)

resourceDocs += (DynamicEntityOperation.PATCH, splitNameWithBankId) -> ResourceDoc(
Expand Down Expand Up @@ -395,7 +399,8 @@ object DynamicEntityHelper {
),
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
Some(List(dynamicEntityInfo.canUpdateRole)),
createdByBankId= dynamicEntityInfo.bankId
createdByBankId= dynamicEntityInfo.bankId,
authMode = dynamicEntityInfo.endpointAuthMode
)

resourceDocs += (DynamicEntityOperation.DELETE, splitNameWithBankId) -> ResourceDoc(
Expand All @@ -421,7 +426,8 @@ object DynamicEntityHelper {
),
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
Some(List(dynamicEntityInfo.canDeleteRole)),
createdByBankId= dynamicEntityInfo.bankId
createdByBankId= dynamicEntityInfo.bankId,
authMode = dynamicEntityInfo.endpointAuthMode
)

if(hasPersonalEntity){ //only hasPersonalEntity == true, then create the myEndpoints
Expand Down Expand Up @@ -742,7 +748,15 @@ object DynamicEntityHelper {
|""".stripMargin

}
case class DynamicEntityInfo(definition: String, entityName: String, bankId: Option[String], hasPersonalEntity: Boolean, hasPublicAccess: Boolean = false, hasCommunityAccess: Boolean = false, personalRequiresRole: Boolean = false, useRowLevelAccess: Boolean = false) {
case class DynamicEntityInfo(definition: String, entityName: String, bankId: Option[String], hasPersonalEntity: Boolean, hasPublicAccess: Boolean = false, hasCommunityAccess: Boolean = false, personalRequiresRole: Boolean = false, useRowLevelAccess: Boolean = false, authMode: String = code.dynamicEntity.DynamicEntityAuthMode.default) {

/** The entity's auth mode as the framework type; unknown or empty values read as UserOnly. */
val endpointAuthMode: code.api.util.APIUtil.EndpointAuthMode = authMode match {
case code.dynamicEntity.DynamicEntityAuthMode.ApplicationOnly => code.api.util.APIUtil.ApplicationOnly
case code.dynamicEntity.DynamicEntityAuthMode.UserOrApplication => code.api.util.APIUtil.UserOrApplication
case code.dynamicEntity.DynamicEntityAuthMode.UserAndApplication => code.api.util.APIUtil.UserAndApplication
case _ => code.api.util.APIUtil.UserOnly
}

import com.openbankproject.commons.util.json
import code.api.dynamic.entity.query.FieldSpec
Expand Down
15 changes: 13 additions & 2 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
}

@deprecated("Use handleAccessControlRegardingEntitlementsAndScopes instead. It checks virtual roles (super_admin, oidc_operator), Scopes, and just-in-time entitlements in addition to Entitlements.", "OBP v6.0.0")
/**
* A consent user (the per-consent principal a Consent-JWT authenticates as) never gets
* just-in-time entitlements. Its roles come from the consent alone: a consent may carry
* CanCreateEntitlementAtOneBank so the agent can grant bank roles to humans, and
* addEntitlement redirects any grant aimed at a consent user to its granting human. Without
* this guard the JIT path would call addEntitlement, see the redirected row as a success,
* and let the consent user through with a role the consent never named.
*/
def isConsentUser(userId: String): Boolean =
Users.users.vend.getUserByUserId(userId).exists(_.isConsentUser)

def hasEntitlement(bankId: String, userId: String, apiRole: ApiRole): Boolean = apiRole match {
case RoleCombination(roles) => roles.forall(hasEntitlement(bankId, userId, _))
case role =>
Expand Down Expand Up @@ -2316,7 +2327,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
def userHasTheRoles: Boolean = {
val userHasTheRole: Boolean = roles.exists(hasEntitlement(bankId, userId, _))
userHasTheRole || {
getPropsAsBoolValue("create_just_in_time_entitlements", false) && {
getPropsAsBoolValue("create_just_in_time_entitlements", false) && !isConsentUser(userId) && {
// If a user is trying to use a Role and the user could grant them selves the required Role(s),
// then just automatically grant the Role(s)!
(hasEntitlement(bankId, userId, ApiRole.canCreateEntitlementAtOneBank) ||
Expand Down Expand Up @@ -2377,7 +2388,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
def userHasTheRoles: Boolean = {
val userHasTheRole: Boolean = roles.exists(hasEntitlement(bankId, userId, _))
userHasTheRole || {
getPropsAsBoolValue("create_just_in_time_entitlements", false) && {
getPropsAsBoolValue("create_just_in_time_entitlements", false) && !isConsentUser(userId) && {
(hasEntitlement(bankId, userId, ApiRole.canCreateEntitlementAtOneBank) ||
hasEntitlement("", userId, ApiRole.canCreateEntitlementAtAnyBank)) &&
roles.forall { role =>
Expand Down
18 changes: 18 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 @@ -776,6 +776,24 @@ object ApiRole extends MdcLoggable{
case class CanDeleteApiProductAttribute(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteApiProductAttribute = CanDeleteApiProductAttribute()

// API Product Subscription roles, held at the product's bank (a billing adapter serving several
// banks is granted the role at each of them). Developers need none of these for their own
// consumers: ownership is enforced in the handler. See API_PRODUCT_SUBSCRIPTION_PLAN.md.
case class CanCreateApiProductSubscriptionAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canCreateApiProductSubscriptionAtOneBank = CanCreateApiProductSubscriptionAtOneBank()
case class CanGetApiProductSubscriptionAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canGetApiProductSubscriptionAtOneBank = CanGetApiProductSubscriptionAtOneBank()
case class CanUpdateApiProductSubscriptionStatusAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canUpdateApiProductSubscriptionStatusAtOneBank = CanUpdateApiProductSubscriptionStatusAtOneBank()
case class CanDeleteApiProductSubscriptionAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteApiProductSubscriptionAtOneBank = CanDeleteApiProductSubscriptionAtOneBank()
case class CanCreateApiProductSubscriptionAttributeAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canCreateApiProductSubscriptionAttributeAtOneBank = CanCreateApiProductSubscriptionAttributeAtOneBank()
case class CanUpdateApiProductSubscriptionAttributeAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canUpdateApiProductSubscriptionAttributeAtOneBank = CanUpdateApiProductSubscriptionAttributeAtOneBank()
case class CanDeleteApiProductSubscriptionAttributeAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canDeleteApiProductSubscriptionAttributeAtOneBank = CanDeleteApiProductSubscriptionAttributeAtOneBank()

case class CanCreateSystemView(requiresBankId: Boolean = false) extends ApiRole
lazy val canCreateSystemView = CanCreateSystemView()
case class CanUpdateSystemView(requiresBankId: Boolean = false) extends ApiRole
Expand Down
Loading
Loading