From 79ea9231a19ab723f732413beb3a7f5d61f6aab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 23 Jan 2026 12:41:26 +0100 Subject: [PATCH] Merge upstream/develop into develop after conflict resolution --- .../scala/code/abacrule/AbacRuleEngine.scala | 462 ++++++++---------- .../src/main/scala/code/abacrule/README.md | 129 ++--- 2 files changed, 222 insertions(+), 369 deletions(-) diff --git a/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala b/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala index 057193ec5..93fb81537 100644 --- a/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala +++ b/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala @@ -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) } } diff --git a/obp-api/src/main/scala/code/abacrule/README.md b/obp-api/src/main/scala/code/abacrule/README.md index c42859498..f845490be 100644 --- a/obp-api/src/main/scala/code/abacrule/README.md +++ b/obp-api/src/main/scala/code/abacrule/README.md @@ -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