Merge upstream/develop into develop after conflict resolution

This commit is contained in:
Marko Milić 2026-01-23 12:41:26 +01:00
parent e0f747a64c
commit 79ea9231a1
2 changed files with 222 additions and 369 deletions

View File

@ -13,7 +13,7 @@ import net.liftweb.util.Helpers.tryo
import java.util.concurrent.ConcurrentHashMap
import scala.collection.JavaConverters._
import scala.collection.concurrent
import scala.concurrent.{Await, Future}
import scala.concurrent.Await
import scala.concurrent.duration._
/**
@ -112,221 +112,190 @@ object AbacRuleEngine {
transactionId: Option[String] = None,
transactionRequestId: Option[String] = None,
customerId: Option[String] = None
): Future[Box[Boolean]] = {
val ruleBox = MappedAbacRuleProvider.getAbacRuleById(ruleId)
ruleBox match {
case Failure(msg, ex, chain) => Future.successful(Failure(msg, ex, chain))
case Empty => Future.successful(Empty)
case Full(rule) =>
if (!rule.isActive) {
Future.successful(Failure(s"ABAC Rule ${rule.ruleName} is not active"))
} else {
// Fetch authenticated user
val authenticatedUserBox = Users.users.vend.getUserByUserId(authenticatedUserId)
authenticatedUserBox match {
case Failure(msg, ex, chain) => Future.successful(Failure(msg, ex, chain))
case Empty => Future.successful(Empty)
case Full(authenticatedUser) =>
// Create futures for all async operations
val authenticatedUserAttributesFuture =
code.api.util.NewStyle.function.getNonPersonalUserAttributes(authenticatedUserId, Some(callContext)).map(_._1)
val authenticatedUserAuthContextFuture =
code.api.util.NewStyle.function.getUserAuthContexts(authenticatedUserId, Some(callContext)).map(_._1)
val authenticatedUserEntitlementsFuture =
code.api.util.NewStyle.function.getEntitlementsByUserId(authenticatedUserId, Some(callContext))
val onBehalfOfUserFuture = onBehalfOfUserId match {
case Some(obUserId) => Future.successful(Users.users.vend.getUserByUserId(obUserId).map(Some(_)))
case None => Future.successful(Full(None))
}
val onBehalfOfUserAttributesFuture = onBehalfOfUserId match {
case Some(obUserId) =>
code.api.util.NewStyle.function.getNonPersonalUserAttributes(obUserId, Some(callContext)).map(_._1)
case None => Future.successful(List.empty[UserAttributeTrait])
}
val onBehalfOfUserAuthContextFuture = onBehalfOfUserId match {
case Some(obUserId) =>
code.api.util.NewStyle.function.getUserAuthContexts(obUserId, Some(callContext)).map(_._1)
case None => Future.successful(List.empty[UserAuthContext])
}
val onBehalfOfUserEntitlementsFuture = onBehalfOfUserId match {
case Some(obUserId) =>
code.api.util.NewStyle.function.getEntitlementsByUserId(obUserId, Some(callContext))
case None => Future.successful(List.empty[Entitlement])
}
val userFuture = userId match {
case Some(uId) => Future.successful(Users.users.vend.getUserByUserId(uId).map(Some(_)))
case None => Future.successful(Full(None))
}
val userAttributesFuture = userId match {
case Some(uId) =>
code.api.util.NewStyle.function.getNonPersonalUserAttributes(uId, Some(callContext)).map(_._1)
case None => Future.successful(List.empty[UserAttributeTrait])
}
val bankFuture = bankId match {
case Some(bId) =>
code.api.util.NewStyle.function.getBank(BankId(bId), Some(callContext)).map(_._1).map(bank => Full(Some(bank))).recover {
case _ => Full(None)
}
case None => Future.successful(Full(None))
}
val bankAttributesFuture = bankId match {
case Some(bId) =>
code.api.util.NewStyle.function.getBankAttributesByBank(BankId(bId), Some(callContext)).map(_._1)
case None => Future.successful(List.empty[BankAttributeTrait])
}
val accountFuture = (bankId, accountId) match {
case (Some(bId), Some(aId)) =>
code.api.util.NewStyle.function.getBankAccount(BankId(bId), AccountId(aId), Some(callContext)).map(_._1).map(account => Full(Some(account))).recover {
case _ => Full(None)
}
case _ => Future.successful(Full(None))
}
val accountAttributesFuture = (bankId, accountId) match {
case (Some(bId), Some(aId)) =>
code.api.util.NewStyle.function.getAccountAttributesByAccount(BankId(bId), AccountId(aId), Some(callContext)).map(_._1)
case _ => Future.successful(List.empty[AccountAttribute])
}
val transactionFuture = (bankId, accountId, transactionId) match {
case (Some(bId), Some(aId), Some(tId)) =>
code.api.util.NewStyle.function.getTransaction(BankId(bId), AccountId(aId), TransactionId(tId), Some(callContext)).map(_._1).map(trans => Full(Some(trans))).recover {
case _ => Full(None)
}
case _ => Future.successful(Full(None))
}
val transactionAttributesFuture = (bankId, transactionId) match {
case (Some(bId), Some(tId)) =>
code.api.util.NewStyle.function.getTransactionAttributes(BankId(bId), TransactionId(tId), Some(callContext)).map(_._1)
case _ => Future.successful(List.empty[TransactionAttribute])
}
val transactionRequestFuture = transactionRequestId match {
case Some(trId) =>
code.api.util.NewStyle.function.getTransactionRequestImpl(TransactionRequestId(trId), Some(callContext)).map(_._1).map(tr => Full(Some(tr))).recover {
case _ => Full(None)
}
case _ => Future.successful(Full(None))
}
val transactionRequestAttributesFuture = (bankId, transactionRequestId) match {
case (Some(bId), Some(trId)) =>
code.api.util.NewStyle.function.getTransactionRequestAttributes(BankId(bId), TransactionRequestId(trId), Some(callContext)).map(_._1)
case _ => Future.successful(List.empty[TransactionRequestAttributeTrait])
}
val customerFuture = (bankId, customerId) match {
case (Some(bId), Some(cId)) =>
code.api.util.NewStyle.function.getCustomerByCustomerId(cId, Some(callContext)).map(_._1).map(cust => Full(Some(cust))).recover {
case _ => Full(None)
}
case _ => Future.successful(Full(None))
}
val customerAttributesFuture = (bankId, customerId) match {
case (Some(bId), Some(cId)) =>
code.api.util.NewStyle.function.getCustomerAttributes(BankId(bId), CustomerId(cId), Some(callContext)).map(_._1)
case _ => Future.successful(List.empty[CustomerAttribute])
}
// Combine all futures
for {
authenticatedUserAttributes <- authenticatedUserAttributesFuture
authenticatedUserAuthContext <- authenticatedUserAuthContextFuture
authenticatedUserEntitlements <- authenticatedUserEntitlementsFuture
onBehalfOfUserOpt <- onBehalfOfUserFuture
onBehalfOfUserAttributes <- onBehalfOfUserAttributesFuture
onBehalfOfUserAuthContext <- onBehalfOfUserAuthContextFuture
onBehalfOfUserEntitlements <- onBehalfOfUserEntitlementsFuture
userOpt <- userFuture
userAttributes <- userAttributesFuture
bankOpt <- bankFuture
bankAttributes <- bankAttributesFuture
accountOpt <- accountFuture
accountAttributes <- accountAttributesFuture
transactionOpt <- transactionFuture
transactionAttributes <- transactionAttributesFuture
transactionRequestOpt <- transactionRequestFuture
transactionRequestAttributes <- transactionRequestAttributesFuture
customerOpt <- customerFuture
customerAttributes <- customerAttributesFuture
} yield {
// Compile and execute the rule
val compiledFuncBox = compileRule(ruleId, rule.ruleCode)
compiledFuncBox.flatMap { compiledFunc =>
(for {
onBehalfOfUser <- onBehalfOfUserOpt
user <- userOpt
bank <- bankOpt
account <- accountOpt
transaction <- transactionOpt
transactionRequest <- transactionRequestOpt
customer <- customerOpt
} yield {
tryo {
compiledFunc(authenticatedUser, authenticatedUserAttributes, authenticatedUserAuthContext, authenticatedUserEntitlements, onBehalfOfUser, onBehalfOfUserAttributes, onBehalfOfUserAuthContext, onBehalfOfUserEntitlements, user, userAttributes, bank, bankAttributes, account, accountAttributes, transaction, transactionAttributes, transactionRequest, transactionRequestAttributes, customer, customerAttributes, Some(callContext))
}
}).flatten
}
}
}
}
}
}
/**
* Synchronous wrapper for executeRule - DEPRECATED
* This function blocks the thread and should be avoided. Use the async version instead.
*
* @deprecated Use the async executeRule that returns Future[Box[Boolean]] instead
*/
@deprecated("Use async executeRule that returns Future[Box[Boolean]]", "6.0.0")
def executeRuleSync(
ruleId: String,
authenticatedUserId: String,
onBehalfOfUserId: Option[String] = None,
userId: Option[String] = None,
callContext: CallContext,
bankId: Option[String] = None,
accountId: Option[String] = None,
viewId: Option[String] = None,
transactionId: Option[String] = None,
transactionRequestId: Option[String] = None,
customerId: Option[String] = None
): Box[Boolean] = {
try {
Await.result(executeRule(
ruleId = ruleId,
authenticatedUserId = authenticatedUserId,
onBehalfOfUserId = onBehalfOfUserId,
userId = userId,
callContext = callContext,
bankId = bankId,
accountId = accountId,
viewId = viewId,
transactionId = transactionId,
transactionRequestId = transactionRequestId,
customerId = customerId
), 30.seconds)
} catch {
case _: java.util.concurrent.TimeoutException =>
Failure("ABAC rule execution timed out")
case ex: Exception =>
Failure(s"ABAC rule execution failed: ${ex.getMessage}")
}
for {
rule <- MappedAbacRuleProvider.getAbacRuleById(ruleId)
_ <- if (rule.isActive) Full(true) else Failure(s"ABAC Rule ${rule.ruleName} is not active")
// Fetch authenticated user (the actual person logged in)
authenticatedUser <- Users.users.vend.getUserByUserId(authenticatedUserId)
// Fetch non-personal attributes for authenticated user
authenticatedUserAttributes = Await.result(
code.api.util.NewStyle.function.getNonPersonalUserAttributes(authenticatedUserId, Some(callContext)).map(_._1),
5.seconds
)
// Fetch auth context for authenticated user
authenticatedUserAuthContext = Await.result(
code.api.util.NewStyle.function.getUserAuthContexts(authenticatedUserId, Some(callContext)).map(_._1),
5.seconds
)
// Fetch entitlements for authenticated user
authenticatedUserEntitlements = Await.result(
code.api.util.NewStyle.function.getEntitlementsByUserId(authenticatedUserId, Some(callContext)),
5.seconds
)
// Fetch onBehalfOf user if provided (delegation scenario)
onBehalfOfUserOpt <- onBehalfOfUserId match {
case Some(obUserId) => Users.users.vend.getUserByUserId(obUserId).map(Some(_))
case None => Full(None)
}
// Fetch attributes for onBehalfOf user if provided
onBehalfOfUserAttributes = onBehalfOfUserId match {
case Some(obUserId) =>
Await.result(
code.api.util.NewStyle.function.getNonPersonalUserAttributes(obUserId, Some(callContext)).map(_._1),
5.seconds
)
case None => List.empty[UserAttributeTrait]
}
// Fetch auth context for onBehalfOf user if provided
onBehalfOfUserAuthContext = onBehalfOfUserId match {
case Some(obUserId) =>
Await.result(
code.api.util.NewStyle.function.getUserAuthContexts(obUserId, Some(callContext)).map(_._1),
5.seconds
)
case None => List.empty[UserAuthContext]
}
// Fetch entitlements for onBehalfOf user if provided
onBehalfOfUserEntitlements = onBehalfOfUserId match {
case Some(obUserId) =>
Await.result(
code.api.util.NewStyle.function.getEntitlementsByUserId(obUserId, Some(callContext)),
5.seconds
)
case None => List.empty[Entitlement]
}
// Fetch target user if userId is provided
userOpt <- userId match {
case Some(uId) => Users.users.vend.getUserByUserId(uId).map(Some(_))
case None => Full(None)
}
// Fetch attributes for target user if provided
userAttributes = userId match {
case Some(uId) =>
Await.result(
code.api.util.NewStyle.function.getNonPersonalUserAttributes(uId, Some(callContext)).map(_._1),
5.seconds
)
case None => List.empty[UserAttributeTrait]
}
// Fetch bank if bankId is provided
bankOpt <- bankId match {
case Some(bId) =>
tryo(Await.result(
code.api.util.NewStyle.function.getBank(BankId(bId), Some(callContext)).map(_._1),
5.seconds
)).map(Some(_))
case None => Full(None)
}
// Fetch bank attributes if bank is provided
bankAttributes = bankId match {
case Some(bId) =>
Await.result(
code.api.util.NewStyle.function.getBankAttributesByBank(BankId(bId), Some(callContext)).map(_._1),
5.seconds
)
case None => List.empty[BankAttributeTrait]
}
// Fetch account if accountId and bankId are provided
accountOpt <- (bankId, accountId) match {
case (Some(bId), Some(aId)) =>
tryo(Await.result(
code.api.util.NewStyle.function.getBankAccount(BankId(bId), AccountId(aId), Some(callContext)).map(_._1),
5.seconds
)).map(Some(_))
case _ => Full(None)
}
// Fetch account attributes if account is provided
accountAttributes = (bankId, accountId) match {
case (Some(bId), Some(aId)) =>
Await.result(
code.api.util.NewStyle.function.getAccountAttributesByAccount(BankId(bId), AccountId(aId), Some(callContext)).map(_._1),
5.seconds
)
case _ => List.empty[AccountAttribute]
}
// Fetch transaction if transactionId, accountId, and bankId are provided
transactionOpt <- (bankId, accountId, transactionId) match {
case (Some(bId), Some(aId), Some(tId)) =>
tryo(Await.result(
code.api.util.NewStyle.function.getTransaction(BankId(bId), AccountId(aId), TransactionId(tId), Some(callContext)).map(_._1),
5.seconds
)).map(trans => Some(trans))
case _ => Full(None)
}
// Fetch transaction attributes if transaction is provided
transactionAttributes = (bankId, transactionId) match {
case (Some(bId), Some(tId)) =>
Await.result(
code.api.util.NewStyle.function.getTransactionAttributes(BankId(bId), TransactionId(tId), Some(callContext)).map(_._1),
5.seconds
)
case _ => List.empty[TransactionAttribute]
}
// Fetch transaction request if transactionRequestId is provided
transactionRequestOpt <- transactionRequestId match {
case Some(trId) =>
tryo(Await.result(
code.api.util.NewStyle.function.getTransactionRequestImpl(TransactionRequestId(trId), Some(callContext)).map(_._1),
5.seconds
)).map(tr => Some(tr))
case _ => Full(None)
}
// Fetch transaction request attributes if transaction request is provided
transactionRequestAttributes = (bankId, transactionRequestId) match {
case (Some(bId), Some(trId)) =>
Await.result(
code.api.util.NewStyle.function.getTransactionRequestAttributes(BankId(bId), TransactionRequestId(trId), Some(callContext)).map(_._1),
5.seconds
)
case _ => List.empty[TransactionRequestAttributeTrait]
}
// Fetch customer if customerId and bankId are provided
customerOpt <- (bankId, customerId) match {
case (Some(bId), Some(cId)) =>
tryo(Await.result(
code.api.util.NewStyle.function.getCustomerByCustomerId(cId, Some(callContext)).map(_._1),
5.seconds
)).map(cust => Some(cust))
case _ => Full(None)
}
// Fetch customer attributes if customer is provided
customerAttributes = (bankId, customerId) match {
case (Some(bId), Some(cId)) =>
Await.result(
code.api.util.NewStyle.function.getCustomerAttributes(BankId(bId), CustomerId(cId), Some(callContext)).map(_._1),
5.seconds
)
case _ => List.empty[CustomerAttribute]
}
// Compile and execute the rule
compiledFunc <- compileRule(ruleId, rule.ruleCode)
result <- tryo {
compiledFunc(authenticatedUser, authenticatedUserAttributes, authenticatedUserAuthContext, authenticatedUserEntitlements, onBehalfOfUserOpt, onBehalfOfUserAttributes, onBehalfOfUserAuthContext, onBehalfOfUserEntitlements, userOpt, userAttributes, bankOpt, bankAttributes, accountOpt, accountAttributes, transactionOpt, transactionAttributes, transactionRequestOpt, transactionRequestAttributes, customerOpt, customerAttributes, Some(callContext))
}
} yield result
}
@ -360,15 +329,15 @@ object AbacRuleEngine {
transactionId: Option[String] = None,
transactionRequestId: Option[String] = None,
customerId: Option[String] = None
): Future[Box[Boolean]] = {
): Box[Boolean] = {
val rules = MappedAbacRuleProvider.getActiveAbacRulesByPolicy(policy)
if (rules.isEmpty) {
// No rules for this policy - default to allow
Future.successful(Full(true))
Full(true)
} else {
// Execute all rules and check if at least one passes
val ruleFutures = rules.map { rule =>
val results = rules.map { rule =>
executeRule(
ruleId = rule.abacRuleId,
authenticatedUserId = authenticatedUserId,
@ -384,59 +353,14 @@ object AbacRuleEngine {
)
}
// Wait for all rule executions to complete
Future.sequence(ruleFutures).map { results =>
// Count successes and failures
val successes = results.filter {
case Full(true) => true
case _ => false
}
// At least one rule must pass (OR logic)
Full(successes.nonEmpty)
// Count successes and failures
val successes = results.filter {
case Full(true) => true
case _ => false
}
}
}
/**
* Synchronous wrapper for executeRulesByPolicy - DEPRECATED
* This function blocks the thread and should be avoided. Use the async version instead.
*
* @deprecated Use async executeRulesByPolicy that returns Future[Box[Boolean]] instead
*/
@deprecated("Use async executeRulesByPolicy that returns Future[Box[Boolean]]", "6.0.0")
def executeRulesByPolicySync(
policy: String,
authenticatedUserId: String,
onBehalfOfUserId: Option[String] = None,
userId: Option[String] = None,
callContext: CallContext,
bankId: Option[String] = None,
accountId: Option[String] = None,
viewId: Option[String] = None,
transactionId: Option[String] = None,
transactionRequestId: Option[String] = None,
customerId: Option[String] = None
): Box[Boolean] = {
try {
Await.result(executeRulesByPolicy(
policy = policy,
authenticatedUserId = authenticatedUserId,
onBehalfOfUserId = onBehalfOfUserId,
userId = userId,
callContext = callContext,
bankId = bankId,
accountId = accountId,
viewId = viewId,
transactionId = transactionId,
transactionRequestId = transactionRequestId,
customerId = customerId
), 30.seconds)
} catch {
case _: java.util.concurrent.TimeoutException =>
Failure("ABAC rules execution timed out")
case ex: Exception =>
Failure(s"ABAC rules execution failed: ${ex.getMessage}")
// At least one rule must pass (OR logic)
Full(successes.nonEmpty)
}
}

View File

@ -177,54 +177,18 @@ val ruleCode = """user.emailAddress.contains("admin")"""
val compiled = AbacRuleEngine.compileRule("rule123", ruleCode)
```
### Execute a Rule (Async - Recommended)
```scala
import code.abacrule.AbacRuleEngine
import com.openbankproject.commons.model._
import scala.concurrent.Future
val resultFuture: Future[Box[Boolean]] = AbacRuleEngine.executeRule(
ruleId = "rule123",
authenticatedUserId = currentUser.userId,
onBehalfOfUserId = None,
userId = Some(targetUser.userId),
callContext = callContext,
bankId = Some(bank.bankId.value),
accountId = Some(account.accountId.value),
viewId = None,
transactionId = None,
transactionRequestId = None,
customerId = None
)
resultFuture.map { result =>
result match {
case Full(true) => println("Access granted")
case Full(false) => println("Access denied")
case Failure(msg, _, _) => println(s"Error: $msg")
case Empty => println("Rule not found")
}
}
```
### Execute a Rule (Sync - Deprecated)
### Execute a Rule
```scala
import code.abacrule.AbacRuleEngine
import com.openbankproject.commons.model._
// This is deprecated and blocks the thread - use async version above instead
val result = AbacRuleEngine.executeRuleSync(
val result = AbacRuleEngine.executeRule(
ruleId = "rule123",
authenticatedUserId = currentUser.userId,
onBehalfOfUserId = None,
userId = Some(targetUser.userId),
callContext = callContext,
bankId = Some(bank.bankId.value),
accountId = Some(account.accountId.value),
viewId = None,
transactionId = None,
transactionRequestId = None,
customerId = None
user = currentUser,
bankOpt = Some(bank),
accountOpt = Some(account),
transactionOpt = None,
customerOpt = None
)
result match {
@ -235,56 +199,24 @@ result match {
}
```
### Execute Rules by Policy (Async - Recommended)
Execute all active rules associated with a policy (OR logic - at least one must pass):
### Execute Multiple Rules (AND Logic)
All rules must pass:
```scala
val resultFuture: Future[Box[Boolean]] = AbacRuleEngine.executeRulesByPolicy(
policy = "account_access_policy",
authenticatedUserId = currentUser.userId,
onBehalfOfUserId = None,
userId = Some(targetUser.userId),
callContext = callContext,
bankId = Some(bank.bankId.value),
accountId = Some(account.accountId.value),
viewId = None,
transactionId = None,
transactionRequestId = None,
customerId = None
val result = AbacRuleEngine.executeRulesAnd(
ruleIds = List("rule1", "rule2", "rule3"),
user = currentUser,
bankOpt = Some(bank)
)
resultFuture.map { result =>
result match {
case Full(true) => println("Access granted by policy")
case Full(false) => println("Access denied by policy")
case Failure(msg, _, _) => println(s"Error: $msg")
case Empty => println("Policy not found")
}
}
```
### Execute Rules by Policy (Sync - Deprecated)
### Execute Multiple Rules (OR Logic)
At least one rule must pass:
```scala
// This is deprecated and blocks the thread - use async version above instead
val result = AbacRuleEngine.executeRulesByPolicySync(
policy = "account_access_policy",
authenticatedUserId = currentUser.userId,
onBehalfOfUserId = None,
userId = Some(targetUser.userId),
callContext = callContext,
bankId = Some(bank.bankId.value),
accountId = Some(account.accountId.value),
viewId = None,
transactionId = None,
transactionRequestId = None,
customerId = None
val result = AbacRuleEngine.executeRulesOr(
ruleIds = List("rule1", "rule2", "rule3"),
user = currentUser,
bankOpt = Some(bank)
)
result match {
case Full(true) => println("Access granted by policy")
case Full(false) => println("Access denied by policy")
case Failure(msg, _, _) => println(s"Error: $msg")
case Empty => println("Policy not found")
}
```
### Validate Rule Code
@ -409,19 +341,16 @@ for {
(account, callContext) <- NewStyle.function.getBankAccount(bankId, accountId, callContext)
// Check ABAC rules
allowed <- AbacRuleEngine.executeRulesByPolicy(
policy = "bank_access_policy",
authenticatedUserId = user.userId,
onBehalfOfUserId = None,
userId = Some(user.userId),
callContext = callContext,
bankId = Some(bank.bankId.value),
accountId = Some(account.accountId.value),
viewId = None,
transactionId = None,
transactionRequestId = None,
customerId = None
)
allowed <- Future {
AbacRuleEngine.executeRulesAnd(
ruleIds = List("bank_access_rule", "account_limit_rule"),
user = user,
bankOpt = Some(bank),
accountOpt = Some(account)
)
} map {
unboxFullOrFail(_, callContext, "ABAC access check failed", 403)
}
_ <- Helper.booleanToFuture(s"Access denied by ABAC rules", cc = callContext) {
allowed