Merge pull request #2658 from simonredfern/develop

Various New Features
This commit is contained in:
Simon Redfern 2026-01-15 10:46:23 +01:00 committed by GitHub
commit efb2d6e7b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 471 additions and 937 deletions

View File

@ -1,35 +0,0 @@
name: Regular base image update check
on:
schedule:
- cron: "0 5 * * *"
workflow_dispatch:
env:
## Sets environment variable
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Docker Image Update Checker
id: baseupdatecheck
uses: lucacome/docker-image-update-checker@v2.0.0
with:
base-image: jetty:9.4-jdk11-alpine
image: ${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api:latest
- name: Trigger build_container_develop_branch workflow
uses: actions/github-script@v6
with:
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build_container_develop_branch.yml',
ref: 'refs/heads/develop'
});
if: steps.baseupdatecheck.outputs.needs-updating == 'true'

View File

@ -124,33 +124,3 @@ jobs:
with:
name: ${{ github.sha }}
path: push/
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file .github/Dockerfile_PreBuild --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop
docker build . --file .github/Dockerfile_PreBuild_OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}-OC
docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }} --all-tags
echo docker done
- uses: sigstore/cosign-installer@4d14d7f17e7112af04ea6108fbb4bfc714c00390
- name: Write signing key to disk (only needed for `cosign sign --key`)
run: echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key
- name: Sign container image
run: |
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:develop-OC
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:latest-OC
env:
COSIGN_PASSWORD: "${{secrets.COSIGN_PASSWORD}}"

View File

@ -1,151 +0,0 @@
name: Build and publish container non develop
on:
push:
branches:
- '**'
- '!develop'
env:
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }}
DOCKER_HUB_REPOSITORY: obp-api
jobs:
build:
runs-on: ubuntu-latest
services:
# Label used to access the service container
redis:
# Docker Hub image
image: redis
ports:
# Opens tcp port 6379 on the host and service container
- 6379:6379
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: maven
- name: Build with Maven
run: |
set -o pipefail
cp obp-api/src/main/resources/props/sample.props.template obp-api/src/main/resources/props/production.default.props
echo connector=star > obp-api/src/main/resources/props/test.default.props
echo starConnector_supported_types=mapped,internal >> obp-api/src/main/resources/props/test.default.props
echo hostname=http://localhost:8016 >> obp-api/src/main/resources/props/test.default.props
echo tests.port=8016 >> obp-api/src/main/resources/props/test.default.props
echo End of minimum settings >> obp-api/src/main/resources/props/test.default.props
echo payments_enabled=false >> obp-api/src/main/resources/props/test.default.props
echo importer_secret=change_me >> obp-api/src/main/resources/props/test.default.props
echo messageQueue.updateBankAccountsTransaction=false >> obp-api/src/main/resources/props/test.default.props
echo messageQueue.createBankAccounts=false >> obp-api/src/main/resources/props/test.default.props
echo allow_sandbox_account_creation=true >> obp-api/src/main/resources/props/test.default.props
echo allow_sandbox_data_import=true >> obp-api/src/main/resources/props/test.default.props
echo sandbox_data_import_secret=change_me >> obp-api/src/main/resources/props/test.default.props
echo allow_account_deletion=true >> obp-api/src/main/resources/props/test.default.props
echo allowed_internal_redirect_urls = /,/oauth/authorize >> obp-api/src/main/resources/props/test.default.props
echo transactionRequests_enabled=true >> obp-api/src/main/resources/props/test.default.props
echo transactionRequests_supported_types=SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,SIMPLE >> obp-api/src/main/resources/props/test.default.props
echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo openredirects.hostname.whitlelist=http://127.0.0.1,http://localhost >> obp-api/src/main/resources/props/test.default.props
echo remotedata.secret = foobarbaz >> obp-api/src/main/resources/props/test.default.props
echo allow_public_views=true >> obp-api/src/main/resources/props/test.default.props
echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo ACCOUNT_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo SEPA_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo FREE_FORM_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo COUNTERPARTY_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo SEPA_CREDIT_TRANSFERS_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo allow_oauth2_login=true >> obp-api/src/main/resources/props/test.default.props
echo oauth2.jwk_set.url=https://www.googleapis.com/oauth2/v3/certs >> obp-api/src/main/resources/props/test.default.props
echo ResetPasswordUrlEnabled=true >> obp-api/src/main/resources/props/test.default.props
echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props
MAVEN_OPTS="-Xmx3G -Xss2m" mvn clean package -Pprod 2>&1 | tee maven-build.log
- name: Report failing tests (if any)
if: always()
run: |
echo "Checking build log for failing tests via grep..."
if [ ! -f maven-build.log ]; then
echo "No maven-build.log found; skipping failure scan."
exit 0
fi
if grep -n "\*\*\* FAILED \*\*\*" maven-build.log; then
echo "Failing tests detected above."
exit 1
else
echo "No failing tests detected in maven-build.log."
fi
- name: Upload Maven build log
if: always()
uses: actions/upload-artifact@v4
with:
name: maven-build-log
if-no-files-found: ignore
path: |
maven-build.log
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
if-no-files-found: ignore
path: |
obp-api/target/surefire-reports/**
obp-commons/target/surefire-reports/**
**/target/scalatest-reports/**
**/target/site/surefire-report.html
**/target/site/surefire-report/*
- name: Save .war artifact
run: |
mkdir -p ./push
cp obp-api/target/obp-api-1.*.war ./push/
- uses: actions/upload-artifact@v4
with:
name: ${{ github.sha }}
path: push/
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file .github/Dockerfile_PreBuild --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}
docker build . --file .github/Dockerfile_PreBuild_OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA-OC --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}-OC
docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }} --all-tags
echo docker done
- uses: sigstore/cosign-installer@4d14d7f17e7112af04ea6108fbb4bfc714c00390
- name: Write signing key to disk (only needed for `cosign sign --key`)
run: echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key
- name: Sign container image
run: |
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${GITHUB_REF##*/}-OC
cosign sign -y --key cosign.key \
docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:$GITHUB_SHA
env:
COSIGN_PASSWORD: "${{secrets.COSIGN_PASSWORD}}"

View File

@ -1,124 +0,0 @@
name: Build on Pull Request
on:
pull_request:
branches:
- '**'
env:
## Sets environment variable
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }}
jobs:
build:
runs-on: ubuntu-latest
services:
# Label used to access the service container
redis:
# Docker Hub image
image: redis
ports:
# Opens tcp port 6379 on the host and service container
- 6379:6379
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'adopt'
cache: maven
- name: Build with Maven
run: |
set -o pipefail
cp obp-api/src/main/resources/props/sample.props.template obp-api/src/main/resources/props/production.default.props
echo connector=star > obp-api/src/main/resources/props/test.default.props
echo starConnector_supported_types=mapped,internal >> obp-api/src/main/resources/props/test.default.props
echo hostname=http://localhost:8016 >> obp-api/src/main/resources/props/test.default.props
echo tests.port=8016 >> obp-api/src/main/resources/props/test.default.props
echo End of minimum settings >> obp-api/src/main/resources/props/test.default.props
echo payments_enabled=false >> obp-api/src/main/resources/props/test.default.props
echo importer_secret=change_me >> obp-api/src/main/resources/props/test.default.props
echo messageQueue.updateBankAccountsTransaction=false >> obp-api/src/main/resources/props/test.default.props
echo messageQueue.createBankAccounts=false >> obp-api/src/main/resources/props/test.default.props
echo allow_sandbox_account_creation=true >> obp-api/src/main/resources/props/test.default.props
echo allow_sandbox_data_import=true >> obp-api/src/main/resources/props/test.default.props
echo sandbox_data_import_secret=change_me >> obp-api/src/main/resources/props/test.default.props
echo allow_account_deletion=true >> obp-api/src/main/resources/props/test.default.props
echo allowed_internal_redirect_urls = /,/oauth/authorize >> obp-api/src/main/resources/props/test.default.props
echo transactionRequests_enabled=true >> obp-api/src/main/resources/props/test.default.props
echo transactionRequests_supported_types=SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,SIMPLE >> obp-api/src/main/resources/props/test.default.props
echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo openredirects.hostname.whitlelist=http://127.0.0.1,http://localhost >> obp-api/src/main/resources/props/test.default.props
echo remotedata.secret = foobarbaz >> obp-api/src/main/resources/props/test.default.props
echo allow_public_views=true >> obp-api/src/main/resources/props/test.default.props
echo SIMPLE_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo ACCOUNT_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo SEPA_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo FREE_FORM_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo COUNTERPARTY_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo SEPA_CREDIT_TRANSFERS_OTP_INSTRUCTION_TRANSPORT=dummy >> obp-api/src/main/resources/props/test.default.props
echo allow_oauth2_login=true >> obp-api/src/main/resources/props/test.default.props
echo oauth2.jwk_set.url=https://www.googleapis.com/oauth2/v3/certs >> obp-api/src/main/resources/props/test.default.props
echo ResetPasswordUrlEnabled=true >> obp-api/src/main/resources/props/test.default.props
echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props
MAVEN_OPTS="-Xmx3G -Xss2m" mvn clean package -Pprod 2>&1 | tee maven-build.log
- name: Report failing tests (if any)
if: always()
run: |
echo "Checking build log for failing tests via grep..."
if [ ! -f maven-build.log ]; then
echo "No maven-build.log found; skipping failure scan."
exit 0
fi
if grep -n "\*\*\* FAILED \*\*\*" maven-build.log; then
echo "Failing tests detected above."
exit 1
else
echo "No failing tests detected in maven-build.log."
fi
- name: Upload Maven build log
if: always()
uses: actions/upload-artifact@v4
with:
name: maven-build-log
if-no-files-found: ignore
path: |
maven-build.log
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
if-no-files-found: ignore
path: |
obp-api/target/surefire-reports/**
obp-commons/target/surefire-reports/**
**/target/scalatest-reports/**
**/target/site/surefire-report.html
**/target/site/surefire-report/*
- name: Save .war artifact
run: |
mkdir -p ./pull
cp obp-api/target/obp-api-1.*.war ./pull/
- uses: actions/upload-artifact@v4
with:
name: ${{ github.sha }}
path: pull/

View File

@ -1,54 +0,0 @@
name: scan container image
on:
workflow_run:
workflows:
- Build and publish container develop
- Build and publish container non develop
types:
- completed
env:
## Sets environment variable
DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }}
DOCKER_HUB_REPOSITORY: obp-api
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@v4
- id: trivy-db
name: Check trivy db sha
env:
GH_TOKEN: ${{ github.token }}
run: |
endpoint='/orgs/aquasecurity/packages/container/trivy-db/versions'
headers='Accept: application/vnd.github+json'
jqFilter='.[] | select(.metadata.container.tags[] | contains("latest")) | .name | sub("sha256:";"")'
sha=$(gh api -H "${headers}" "${endpoint}" | jq --raw-output "${jqFilter}")
echo "Trivy DB sha256:${sha}"
echo "::set-output name=sha::${sha}"
- uses: actions/cache@v4
with:
path: .trivy
key: ${{ runner.os }}-trivy-db-${{ steps.trivy-db.outputs.sha }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/${{ env.DOCKER_HUB_REPOSITORY }}:${{ github.sha }}'
format: 'template'
template: '@/contrib/sarif.tpl'
output: 'trivy-results.sarif'
security-checks: 'vuln'
severity: 'CRITICAL,HIGH'
timeout: '30m'
cache-dir: .trivy
- name: Fix .trivy permissions
run: sudo chown -R $(stat . -c %u:%g) .trivy
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'

View File

@ -4,6 +4,7 @@ import code.api.util.{APIUtil, CallContext, DynamicUtil}
import code.bankconnectors.Connector
import code.model.dataAccess.ResourceUser
import code.users.Users
import code.entitlement.Entitlement
import com.openbankproject.commons.model._
import com.openbankproject.commons.ExecutionContext.Implicits.global
import net.liftweb.common.{Box, Empty, Failure, Full}
@ -26,12 +27,12 @@ object AbacRuleEngine {
/**
* Type alias for compiled ABAC rule function
* Parameters: authenticatedUser (logged in), authenticatedUserAttributes (non-personal), authenticatedUserAuthContext (auth context),
* onBehalfOfUser (delegation), onBehalfOfUserAttributes, onBehalfOfUserAuthContext,
* Parameters: authenticatedUser (logged in), authenticatedUserAttributes (non-personal), authenticatedUserAuthContext (auth context), authenticatedUserEntitlements (roles),
* onBehalfOfUser (delegation), onBehalfOfUserAttributes, onBehalfOfUserAuthContext, onBehalfOfUserEntitlements,
* user, userAttributes, bankOpt, bankAttributes, accountOpt, accountAttributes, transactionOpt, transactionAttributes, customerOpt, customerAttributes
* Returns: Boolean (true = allow access, false = deny access)
*/
type AbacRuleFunction = (User, List[UserAttributeTrait], List[UserAuthContext], Option[User], List[UserAttributeTrait], List[UserAuthContext], Option[User], List[UserAttributeTrait], Option[Bank], List[BankAttributeTrait], Option[BankAccount], List[AccountAttribute], Option[Transaction], List[TransactionAttribute], Option[TransactionRequest], List[TransactionRequestAttributeTrait], Option[Customer], List[CustomerAttribute], Option[CallContext]) => Boolean
type AbacRuleFunction = (User, List[UserAttributeTrait], List[UserAuthContext], List[Entitlement], Option[User], List[UserAttributeTrait], List[UserAuthContext], List[Entitlement], Option[User], List[UserAttributeTrait], Option[Bank], List[BankAttributeTrait], Option[BankAccount], List[AccountAttribute], Option[Transaction], List[TransactionAttribute], Option[TransactionRequest], List[TransactionRequestAttributeTrait], Option[Customer], List[CustomerAttribute], Option[CallContext]) => Boolean
/**
* Compile an ABAC rule from Scala code
@ -73,9 +74,11 @@ object AbacRuleEngine {
|import com.openbankproject.commons.model._
|import code.model.dataAccess.ResourceUser
|import net.liftweb.common._
|import code.entitlement.Entitlement
|import code.api.util.CallContext
|
|// ABAC Rule Function
|(authenticatedUser: User, authenticatedUserAttributes: List[UserAttributeTrait], authenticatedUserAuthContext: List[UserAuthContext], onBehalfOfUserOpt: Option[User], onBehalfOfUserAttributes: List[UserAttributeTrait], onBehalfOfUserAuthContext: List[UserAuthContext], userOpt: Option[User], userAttributes: List[UserAttributeTrait], bankOpt: Option[Bank], bankAttributes: List[BankAttributeTrait], accountOpt: Option[BankAccount], accountAttributes: List[AccountAttribute], transactionOpt: Option[Transaction], transactionAttributes: List[TransactionAttribute], transactionRequestOpt: Option[TransactionRequest], transactionRequestAttributes: List[TransactionRequestAttributeTrait], customerOpt: Option[Customer], customerAttributes: List[CustomerAttribute], callContext: Option[code.api.util.CallContext]) => {
|(authenticatedUser: User, authenticatedUserAttributes: List[UserAttributeTrait], authenticatedUserAuthContext: List[UserAuthContext], authenticatedUserEntitlements: List[Entitlement], onBehalfOfUserOpt: Option[User], onBehalfOfUserAttributes: List[UserAttributeTrait], onBehalfOfUserAuthContext: List[UserAuthContext], onBehalfOfUserEntitlements: List[Entitlement], userOpt: Option[User], userAttributes: List[UserAttributeTrait], bankOpt: Option[Bank], bankAttributes: List[BankAttributeTrait], accountOpt: Option[BankAccount], accountAttributes: List[AccountAttribute], transactionOpt: Option[Transaction], transactionAttributes: List[TransactionAttribute], transactionRequestOpt: Option[TransactionRequest], transactionRequestAttributes: List[TransactionRequestAttributeTrait], customerOpt: Option[Customer], customerAttributes: List[CustomerAttribute], callContext: Option[code.api.util.CallContext]) => {
| $ruleCode
|}
|""".stripMargin
@ -129,6 +132,12 @@ object AbacRuleEngine {
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(_))
@ -155,6 +164,16 @@ object AbacRuleEngine {
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(_))
@ -274,13 +293,77 @@ object AbacRuleEngine {
// Compile and execute the rule
compiledFunc <- compileRule(ruleId, rule.ruleCode)
result <- tryo {
compiledFunc(authenticatedUser, authenticatedUserAttributes, authenticatedUserAuthContext, onBehalfOfUserOpt, onBehalfOfUserAttributes, onBehalfOfUserAuthContext, userOpt, userAttributes, bankOpt, bankAttributes, accountOpt, accountAttributes, transactionOpt, transactionAttributes, transactionRequestOpt, transactionRequestAttributes, customerOpt, customerAttributes, Some(callContext))
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
}
/**
* Execute all active ABAC rules with a specific policy (OR logic - at least one must pass)
* @param logic The logic to apply: "AND" (all must pass), "OR" (any must pass), "XOR" (exactly one must pass)
*
* @param policy The policy to filter rules by
* @param authenticatedUserId The ID of the authenticated user
* @param onBehalfOfUserId Optional ID of user being acted on behalf of
* @param userId The ID of the target user to evaluate
* @param callContext Call context for fetching objects
* @param bankId Optional bank ID
* @param accountId Optional account ID
* @param viewId Optional view ID
* @param transactionId Optional transaction ID
* @param transactionRequestId Optional transaction request ID
* @param customerId Optional customer ID
* @return Box[Boolean] - Full(true) if at least one rule passes (OR logic), Full(false) if all fail
*/
def executeRulesByPolicy(
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] = {
val rules = MappedAbacRuleProvider.getActiveAbacRulesByPolicy(policy)
if (rules.isEmpty) {
// No rules for this policy - default to allow
Full(true)
} else {
// Execute all rules and check if at least one passes
val results = rules.map { rule =>
executeRule(
ruleId = rule.abacRuleId,
authenticatedUserId = authenticatedUserId,
onBehalfOfUserId = onBehalfOfUserId,
userId = userId,
callContext = callContext,
bankId = bankId,
accountId = accountId,
viewId = viewId,
transactionId = transactionId,
transactionRequestId = transactionRequestId,
customerId = customerId
)
}
// 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)
}
}
/**
* Validate ABAC rule code by attempting to compile it
*

View File

@ -14,6 +14,7 @@ trait AbacRuleTrait {
def ruleCode: String
def isActive: Boolean
def description: String
def policy: String
def createdByUserId: String
def updatedByUserId: String
}
@ -30,6 +31,7 @@ class AbacRule extends AbacRuleTrait with LongKeyedMapper[AbacRule] with IdPK wi
override def defaultValue = true
}
object Description extends MappedText(this)
object Policy extends MappedText(this)
object CreatedByUserId extends MappedString(this, 255)
object UpdatedByUserId extends MappedString(this, 255)
@ -38,6 +40,7 @@ class AbacRule extends AbacRuleTrait with LongKeyedMapper[AbacRule] with IdPK wi
override def ruleCode: String = RuleCode.get
override def isActive: Boolean = IsActive.get
override def description: String = Description.get
override def policy: String = Policy.get
override def createdByUserId: String = CreatedByUserId.get
override def updatedByUserId: String = UpdatedByUserId.get
}
@ -51,10 +54,13 @@ trait AbacRuleProvider {
def getAbacRuleByName(ruleName: String): Box[AbacRuleTrait]
def getAllAbacRules(): List[AbacRuleTrait]
def getActiveAbacRules(): List[AbacRuleTrait]
def getAbacRulesByPolicy(policy: String): List[AbacRuleTrait]
def getActiveAbacRulesByPolicy(policy: String): List[AbacRuleTrait]
def createAbacRule(
ruleName: String,
ruleCode: String,
description: String,
policy: String,
isActive: Boolean,
createdBy: String
): Box[AbacRuleTrait]
@ -63,6 +69,7 @@ trait AbacRuleProvider {
ruleName: String,
ruleCode: String,
description: String,
policy: String,
isActive: Boolean,
updatedBy: String
): Box[AbacRuleTrait]
@ -87,10 +94,23 @@ object MappedAbacRuleProvider extends AbacRuleProvider {
AbacRule.findAll(By(AbacRule.IsActive, true))
}
override def getAbacRulesByPolicy(policy: String): List[AbacRuleTrait] = {
AbacRule.findAll().filter { rule =>
rule.policy.split(",").map(_.trim).contains(policy)
}
}
override def getActiveAbacRulesByPolicy(policy: String): List[AbacRuleTrait] = {
AbacRule.findAll(By(AbacRule.IsActive, true)).filter { rule =>
rule.policy.split(",").map(_.trim).contains(policy)
}
}
override def createAbacRule(
ruleName: String,
ruleCode: String,
description: String,
policy: String,
isActive: Boolean,
createdBy: String
): Box[AbacRuleTrait] = {
@ -99,6 +119,7 @@ object MappedAbacRuleProvider extends AbacRuleProvider {
.RuleName(ruleName)
.RuleCode(ruleCode)
.Description(description)
.Policy(policy)
.IsActive(isActive)
.CreatedByUserId(createdBy)
.UpdatedByUserId(createdBy)
@ -111,6 +132,7 @@ object MappedAbacRuleProvider extends AbacRuleProvider {
ruleName: String,
ruleCode: String,
description: String,
policy: String,
isActive: Boolean,
updatedBy: String
): Box[AbacRuleTrait] = {
@ -121,6 +143,7 @@ object MappedAbacRuleProvider extends AbacRuleProvider {
.RuleName(ruleName)
.RuleCode(ruleCode)
.Description(description)
.Policy(policy)
.IsActive(isActive)
.UpdatedByUserId(updatedBy)
.saveMe()

View File

@ -1257,3 +1257,4 @@ so the caller must specify any required filtering by catalog explicitly.
}

View File

@ -266,6 +266,19 @@ object Constant extends MdcLoggable {
// ABAC Cache Prefixes (with global namespace and versioning)
def ABAC_RULE_PREFIX: String = getVersionedCachePrefix(ABAC_RULE_NAMESPACE)
// ABAC Policy Constants
final val ABAC_POLICY_ACCOUNT_ACCESS = "account-access"
// List of all ABAC Policies
final val ABAC_POLICIES: List[String] = List(
ABAC_POLICY_ACCOUNT_ACCESS
)
// Map of ABAC Policies to their descriptions
final val ABAC_POLICY_DESCRIPTIONS: Map[String, String] = Map(
ABAC_POLICY_ACCOUNT_ACCESS -> "Rules for controlling access to account information and account-related operations"
)
final val CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT = "can_see_transaction_other_bank_account"
final val CAN_SEE_TRANSACTION_METADATA = "can_see_transaction_metadata"
final val CAN_SEE_TRANSACTION_DESCRIPTION = "can_see_transaction_description"

View File

@ -1638,6 +1638,11 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
var errorResponseBodies: List[String], // Possible error responses
tags: List[ResourceDocTag],
var roles: Option[List[ApiRole]] = None,
// IMPORTANT: Roles declared here are AUTOMATICALLY CHECKED at runtime!
// When roles specified, framework automatically: 1) Validates user authentication,
// 2) Checks user has at least one of specified roles, 3) Performs checks in wrappedWithAuthCheck()
// No manual hasEntitlement() call needed in endpoint body - handled automatically!
// To disable: call .disableAutoValidateRoles() on ResourceDoc
isFeatured: Boolean = false,
specialInstructions: Option[String] = None,
var specifiedUrl: Option[String] = None, // A derived value: Contains the called version (added at run time). See the resource doc for resource doc!

View File

@ -124,7 +124,7 @@ object ApiRole extends MdcLoggable{
// ALL
case class CanGetSystemLogCacheAll(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetSystemLogCacheAll = CanGetSystemLogCacheAll()
case class CanUpdateAgentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
lazy val canUpdateAgentStatusAtAnyBank = CanUpdateAgentStatusAtAnyBank()
@ -1003,6 +1003,9 @@ object ApiRole extends MdcLoggable{
case class CanGetAllConnectorMethods(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetAllConnectorMethods = CanGetAllConnectorMethods()
case class CanGetSystemConnectorMethodNames(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetSystemConnectorMethodNames = CanGetSystemConnectorMethodNames()
case class CanCreateDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole
lazy val canCreateDynamicResourceDoc = CanCreateDynamicResourceDoc()

View File

@ -18,6 +18,8 @@ object ApiTag {
val apiTagTransactionRequestAttribute = ResourceDocTag("Transaction-Request-Attribute")
val apiTagVrp = ResourceDocTag("VRP")
val apiTagApi = ResourceDocTag("API")
val apiTagOAuth = ResourceDocTag("OAuth")
val apiTagOIDC = ResourceDocTag("OIDC")
val apiTagBank = ResourceDocTag("Bank")
val apiTagBankAttribute = ResourceDocTag("Bank-Attribute")
val apiTagAccount = ResourceDocTag("Account")

View File

@ -1864,7 +1864,7 @@ trait APIMethods310 {
List(
UnknownError
),
List(apiTagApi))
List(apiTagApi, apiTagOAuth, apiTagOIDC))
lazy val getObpConnectorLoopback : OBPEndpoint = {
case "connector" :: "loopback" :: Nil JsonGet _ => {
@ -4112,7 +4112,7 @@ trait APIMethods310 {
List(
UnknownError
),
List(apiTagApi))
List(apiTagApi, apiTagOAuth, apiTagOIDC))
lazy val getOAuth2ServerJWKsURIs: OBPEndpoint = {
case "jwks-uris" :: Nil JsonGet _ => {

View File

@ -1075,7 +1075,24 @@ trait APIMethods600 {
entitlements <- NewStyle.function.getEntitlementsByUserId(u.userId, callContext)
} yield {
val permissions: Option[Permission] = Views.views.vend.getPermissionForUser(u).toOption
val currentUser = UserV600(u, entitlements, permissions)
// Add SuperAdmin virtual entitlement if user is super admin
val finalEntitlements = if (APIUtil.isSuperAdmin(u.userId)) {
// Create a virtual SuperAdmin entitlement
val superAdminEntitlement: Entitlement = new Entitlement {
def entitlementId: String = ""
def bankId: String = ""
def userId: String = u.userId
def roleName: String = "SuperAdmin"
def createdByProcess: String = "System"
def entitlementRequestId: Option[String] = None
def groupId: Option[String] = None
def process: Option[String] = None
}
entitlements ::: List(superAdminEntitlement)
} else {
entitlements
}
val currentUser = UserV600(u, finalEntitlements, permissions)
val onBehalfOfUser = if(cc.onBehalfOfUser.isDefined) {
val user = cc.onBehalfOfUser.toOption.get
val entitlements = Entitlement.entitlement.vend.getEntitlementsByUserId(user.userId).headOption.toList.flatten
@ -1649,7 +1666,7 @@ trait APIMethods600 {
|
|${userAuthenticationMessage(true)}
|
|CanGetMethodRoutings entitlement is required.
|CanGetSystemConnectorMethodNames entitlement is required.
|
""".stripMargin,
EmptyBody,
@ -1659,8 +1676,8 @@ trait APIMethods600 {
UserHasMissingRoles,
UnknownError
),
List(apiTagSystem, apiTagMethodRouting, apiTagApi),
Some(List(canGetMethodRoutings))
List(apiTagConnectorMethod, apiTagSystem, apiTagMethodRouting, apiTagApi),
Some(List(canGetSystemConnectorMethodNames))
)
lazy val getConnectorMethodNames: OBPEndpoint = {
@ -1668,7 +1685,6 @@ trait APIMethods600 {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(u), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", u.userId, canGetMethodRoutings, callContext)
// Fetch connector method names with caching
methodNames <- Future {
/**
@ -4734,6 +4750,7 @@ trait APIMethods600 {
rule_name = "admin_only",
rule_code = """user.emailAddress.contains("admin")""",
description = "Only allow access to users with admin email",
policy = "user-access,admin",
is_active = true
),
AbacRuleJsonV600(
@ -4742,6 +4759,7 @@ trait APIMethods600 {
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
policy = "user-access,admin",
created_by_user_id = "user123",
updated_by_user_id = "user123"
),
@ -4781,6 +4799,7 @@ trait APIMethods600 {
ruleName = createJson.rule_name,
ruleCode = createJson.rule_code,
description = createJson.description,
policy = createJson.policy,
isActive = createJson.is_active,
createdBy = user.userId
)
@ -4817,6 +4836,7 @@ trait APIMethods600 {
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
policy = "user-access,admin",
created_by_user_id = "user123",
updated_by_user_id = "user123"
),
@ -4872,6 +4892,7 @@ trait APIMethods600 {
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
policy = "user-access,admin",
created_by_user_id = "user123",
updated_by_user_id = "user123"
)
@ -4901,6 +4922,75 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
getAbacRulesByPolicy,
implementedInApiVersion,
nameOf(getAbacRulesByPolicy),
"GET",
"/management/abac-rules/policy/POLICY",
"Get ABAC Rules by Policy",
s"""Get all ABAC rules that belong to a specific policy.
|
|Multiple rules can share the same policy. Rules with multiple policies (comma-separated)
|will be returned if any of their policies match the requested policy.
|
|**Documentation:**
|- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules
|- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters
|- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
AbacRulesJsonV600(
abac_rules = List(
AbacRuleJsonV600(
abac_rule_id = "abc123",
rule_name = "admin_only",
rule_code = """user.emailAddress.contains("admin")""",
is_active = true,
description = "Only allow access to users with admin email",
policy = "user-access,admin",
created_by_user_id = "user123",
updated_by_user_id = "user123"
),
AbacRuleJsonV600(
abac_rule_id = "def456",
rule_name = "admin_department_check",
rule_code = """user.department == "admin"""",
is_active = true,
description = "Check if user is in admin department",
policy = "admin",
created_by_user_id = "user123",
updated_by_user_id = "user123"
)
)
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagABAC),
Some(List(canGetAbacRule))
)
lazy val getAbacRulesByPolicy: OBPEndpoint = {
case "management" :: "abac-rules" :: "policy" :: policy :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext)
rules <- Future {
MappedAbacRuleProvider.getAbacRulesByPolicy(policy)
}
} yield {
(createAbacRulesJsonV600(rules), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
updateAbacRule,
implementedInApiVersion,
@ -4922,6 +5012,7 @@ trait APIMethods600 {
rule_name = "admin_only_updated",
rule_code = """user.emailAddress.contains("admin") && user.provider == "obp"""",
description = "Only allow access to OBP admin users",
policy = "user-access,admin,obp",
is_active = true
),
AbacRuleJsonV600(
@ -4930,6 +5021,7 @@ trait APIMethods600 {
rule_code = """user.emailAddress.contains("admin") && user.provider == "obp"""",
is_active = true,
description = "Only allow access to OBP admin users",
policy = "user-access,admin,obp",
created_by_user_id = "user123",
updated_by_user_id = "user456"
),
@ -4964,6 +5056,7 @@ trait APIMethods600 {
ruleName = updateJson.rule_name,
ruleCode = updateJson.rule_code,
description = updateJson.description,
policy = updateJson.policy,
isActive = updateJson.is_active,
updatedBy = user.userId
)
@ -5078,16 +5171,18 @@ trait APIMethods600 {
),
examples = List(
AbacRuleExampleJsonV600(
category = "User Access",
title = "Check User Identity",
code = "authenticatedUser.userId == user.userId",
description = "Verify that the authenticated user matches the target user"
rule_name = "Check User Identity",
rule_code = "authenticatedUser.userId == user.userId",
description = "Verify that the authenticated user matches the target user",
policy = "user-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "Bank Access",
title = "Check Specific Bank",
code = "bankOpt.isDefined && bankOpt.get.bankId.value == \"gh.29.uk\"",
description = "Verify that the bank context is defined and matches a specific bank ID"
rule_name = "Check Specific Bank",
rule_code = "bankOpt.isDefined && bankOpt.get.bankId.value == \"gh.29.uk\"",
policy = "bank-access",
description = "Verify that the bank context is defined and matches a specific bank ID",
is_active = true
)
),
available_operators = List("==", "!=", "&&", "||", "!", ">", "<", ">=", "<=", "contains", "isDefined"),
@ -5118,9 +5213,11 @@ trait APIMethods600 {
AbacParameterJsonV600("authenticatedUser", "User", "The logged-in user (always present)", required = true, "User"),
AbacParameterJsonV600("authenticatedUserAttributes", "List[UserAttributeTrait]", "Non-personal attributes of authenticated user", required = true, "User"),
AbacParameterJsonV600("authenticatedUserAuthContext", "List[UserAuthContext]", "Auth context of authenticated user", required = true, "User"),
AbacParameterJsonV600("authenticatedUserEntitlements", "List[Entitlement]", "Entitlements (roles) of authenticated user", required = true, "User"),
AbacParameterJsonV600("onBehalfOfUserOpt", "Option[User]", "User being acted on behalf of (delegation)", required = false, "User"),
AbacParameterJsonV600("onBehalfOfUserAttributes", "List[UserAttributeTrait]", "Attributes of delegation user", required = false, "User"),
AbacParameterJsonV600("onBehalfOfUserAuthContext", "List[UserAuthContext]", "Auth context of delegation user", required = false, "User"),
AbacParameterJsonV600("onBehalfOfUserEntitlements", "List[Entitlement]", "Entitlements (roles) of delegation user", required = false, "User"),
AbacParameterJsonV600("userOpt", "Option[User]", "Target user being evaluated", required = false, "User"),
AbacParameterJsonV600("userAttributes", "List[UserAttributeTrait]", "Attributes of target user", required = false, "User"),
AbacParameterJsonV600("bankOpt", "Option[Bank]", "Bank context", required = false, "Bank"),
@ -5225,6 +5322,12 @@ trait APIMethods600 {
AbacObjectPropertyJsonV600("value", "String", "Attribute value"),
AbacObjectPropertyJsonV600("attributeType", "AttributeType", "Attribute type")
)),
AbacObjectTypeJsonV600("Entitlement", "User entitlement (role)", List(
AbacObjectPropertyJsonV600("entitlementId", "String", "Entitlement ID"),
AbacObjectPropertyJsonV600("roleName", "String", "Role name (e.g., CanCreateAccount, CanReadTransactions)"),
AbacObjectPropertyJsonV600("bankId", "String", "Bank ID (empty string for system-wide roles)"),
AbacObjectPropertyJsonV600("userId", "String", "User ID this entitlement belongs to")
)),
AbacObjectTypeJsonV600("CallContext", "Request context with metadata", List(
AbacObjectPropertyJsonV600("correlationId", "String", "Correlation ID for request tracking"),
AbacObjectPropertyJsonV600("url", "Option[String]", "Request URL"),
@ -5238,538 +5341,60 @@ trait APIMethods600 {
),
examples = List(
AbacRuleExampleJsonV600(
category = "User - Authenticated User",
title = "Check Email Domain",
code = "authenticatedUser.emailAddress.contains(\"@example.com\")",
description = "Verify that the authenticated user's email belongs to a specific domain"
rule_name = "Branch Manager Internal Account Access",
rule_code = "authenticatedUserEntitlements.exists(e => e.roleName == \"CanReadAccountsAtOneBank\") && authenticatedUserAttributes.exists(a => a.name == \"branch\" && accountAttributes.exists(aa => aa.name == \"branch\" && a.value == aa.value)) && callContext.exists(_.verb.exists(_ == \"GET\")) && accountOpt.exists(_.accountType == \"CURRENT\")",
description = "Allow GET access to current accounts when user has CanReadAccountsAtOneBank role and branch matches account's branch",
policy = "account-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User - Authenticated User",
title = "Check Authentication Provider",
code = "authenticatedUser.provider == \"obp\"",
description = "Verify the authentication provider is OBP"
rule_name = "Internal Network High-Value Transaction Review",
rule_code = "callContext.exists(_.ipAddress.exists(_.startsWith(\"10.\"))) && authenticatedUserEntitlements.exists(e => e.roleName == \"CanReadTransactionsAtOneBank\") && transactionOpt.exists(_.amount > 10000)",
description = "Allow users with CanReadTransactionsAtOneBank role on internal network to review high-value transactions over 10,000",
policy = "transaction-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User - Authenticated User",
title = "Compare Authenticated to Target User",
code = "authenticatedUser.userId == userOpt.get.userId",
description = "Check if authenticated user matches the target user (unsafe - use exists instead)"
rule_name = "Department Head Same-Department Account Read where overdrawn",
rule_code = "authenticatedUserEntitlements.exists(e => e.roleName == \"CanReadAccountsAtOneBank\") && authenticatedUserAttributes.exists(ua => ua.name == \"department\" && accountAttributes.exists(aa => aa.name == \"department\" && ua.value == aa.value)) && callContext.exists(_.url.exists(_.contains(\"/accounts/\"))) && accountOpt.exists(_.balance < 0)",
description = "Allow users with CanReadAccountsAtOneBank role to read overdrawn accounts in their department",
policy = "account-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User - Authenticated User",
title = "Check User Not Deleted",
code = "!authenticatedUser.isDeleted.getOrElse(false)",
description = "Verify the authenticated user is not marked as deleted"
rule_name = "Manager Internal Network Transaction Approval",
rule_code = "authenticatedUserEntitlements.exists(e => e.roleName == \"CanCreateTransactionRequest\") && callContext.exists(_.ipAddress.exists(ip => ip.startsWith(\"10.\") || ip.startsWith(\"192.168.\"))) && transactionRequestOpt.exists(tr => tr.status == \"PENDING\" && tr.charge.value.toDouble < 50000)",
description = "Allow users with CanCreateTransactionRequest role on internal network to approve pending transaction requests under 50,000",
policy = "transaction-request",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User Attributes - Authenticated User",
title = "Check Admin Role",
code = "authenticatedUserAttributes.exists(attr => attr.name == \"role\" && attr.value == \"admin\")",
description = "Check if authenticated user has admin role attribute"
rule_name = "KYC Officer Customer Creation from Branch",
rule_code = "authenticatedUserEntitlements.exists(e => e.roleName == \"CanCreateCustomer\") && authenticatedUserAttributes.exists(a => a.name == \"certification\" && a.value == \"kyc_certified\") && callContext.exists(_.verb.exists(_ == \"POST\")) && callContext.exists(_.ipAddress.exists(_.startsWith(\"10.20.\"))) && customerAttributes.exists(ca => ca.name == \"onboarding_status\" && ca.value == \"pending\")",
description = "Allow users with CanCreateCustomer role and KYC certification to create customers via POST from branch network (10.20.x.x) when status is pending",
policy = "customer-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User Attributes - Authenticated User",
title = "Check Department",
code = "authenticatedUserAttributes.find(_.name == \"department\").exists(_.value == \"finance\")",
description = "Check if authenticated user belongs to finance department"
rule_name = "International Team Foreign Currency Transaction",
rule_code = "authenticatedUserEntitlements.exists(e => e.roleName == \"CanReadTransactionsAtOneBank\") && authenticatedUserAttributes.exists(a => a.name == \"team\" && a.value == \"international\") && callContext.exists(_.url.exists(_.contains(\"/transactions/\"))) && transactionOpt.exists(t => t.currency != \"USD\" && t.amount < 100000) && accountOpt.exists(a => accountAttributes.exists(aa => aa.name == \"international_enabled\" && aa.value == \"true\"))",
description = "Allow international team users with CanReadTransactionsAtOneBank role to access foreign currency transactions under 100k on international-enabled accounts",
policy = "transaction-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User Attributes - Authenticated User",
title = "Check Multiple Roles",
code = "authenticatedUserAttributes.exists(attr => attr.name == \"role\" && List(\"admin\", \"manager\").contains(attr.value))",
description = "Check if authenticated user has admin or manager role"
rule_name = "Assistant with Limited Delegation Account View",
rule_code = "onBehalfOfUserOpt.isDefined && onBehalfOfUserEntitlements.exists(e => e.roleName == \"CanReadAccountsAtOneBank\") && authenticatedUserAttributes.exists(a => a.name == \"assistant_of\" && onBehalfOfUserOpt.exists(u => a.value == u.userId)) && callContext.exists(_.verb.exists(_ == \"GET\")) && accountOpt.exists(a => accountAttributes.exists(aa => aa.name == \"tier\" && List(\"gold\", \"platinum\").contains(aa.value)))",
description = "Allow assistants to view gold/platinum accounts via GET when acting on behalf of a user with CanReadAccountsAtOneBank role",
policy = "account-access",
is_active = true
),
AbacRuleExampleJsonV600(
category = "User Auth Context",
title = "Check Session Type",
code = "authenticatedUserAuthContext.exists(_.key == \"session_type\" && _.value == \"secure\")",
description = "Verify the session type is secure"
),
AbacRuleExampleJsonV600(
category = "User Auth Context",
title = "Check Auth Method",
code = "authenticatedUserAuthContext.exists(_.key == \"auth_method\" && _.value == \"certificate\")",
description = "Verify authentication was done via certificate"
),
AbacRuleExampleJsonV600(
category = "User - Delegation",
title = "Check Delegated User Email Domain",
code = "onBehalfOfUserOpt.exists(_.emailAddress.endsWith(\"@company.com\"))",
description = "Check if delegation user belongs to specific company domain"
),
AbacRuleExampleJsonV600(
category = "User - Delegation",
title = "Check No Delegation or Self Delegation",
code = "onBehalfOfUserOpt.isEmpty || onBehalfOfUserOpt.get.userId == authenticatedUser.userId",
description = "Allow if no delegation or user delegating to themselves"
),
AbacRuleExampleJsonV600(
category = "User - Delegation",
title = "Check Different User Delegation",
code = "onBehalfOfUserOpt.forall(_.userId != authenticatedUser.userId)",
description = "Check that delegation is to a different user (if present)"
),
AbacRuleExampleJsonV600(
category = "User Attributes - Delegation",
title = "Check Delegation Level",
code = "onBehalfOfUserAttributes.exists(attr => attr.name == \"delegation_level\" && attr.value == \"full\")",
description = "Check if delegation has full permission level"
),
AbacRuleExampleJsonV600(
category = "User Attributes - Delegation",
title = "Check Authorized Delegation",
code = "onBehalfOfUserAttributes.isEmpty || onBehalfOfUserAttributes.exists(_.name == \"authorized\")",
description = "Allow if no delegation attributes or has authorized attribute"
),
AbacRuleExampleJsonV600(
category = "User - Target User",
title = "Check Self Access",
code = "userOpt.isDefined && userOpt.get.userId == authenticatedUser.userId",
description = "Check if target user is the authenticated user (self-access)"
),
AbacRuleExampleJsonV600(
category = "User - Target User",
title = "Check Target User Provider",
code = "userOpt.exists(_.provider == \"obp\")",
description = "Check if target user is authenticated via OBP provider"
),
AbacRuleExampleJsonV600(
category = "User - Target User",
title = "Check Target User Email Domain",
code = "userOpt.exists(_.emailAddress.endsWith(\"@trusted.com\"))",
description = "Check if target user belongs to trusted domain"
),
AbacRuleExampleJsonV600(
category = "User - Target User",
title = "Check Target User Active",
code = "userOpt.forall(!_.isDeleted.getOrElse(false))",
description = "Ensure target user is not deleted (if present)"
),
AbacRuleExampleJsonV600(
category = "User Attributes - Target User",
title = "Check Premium Account",
code = "userAttributes.exists(attr => attr.name == \"account_type\" && attr.value == \"premium\")",
description = "Check if target user has premium account type"
),
AbacRuleExampleJsonV600(
category = "User Attributes - Target User",
title = "Check KYC Status",
code = "userAttributes.exists(attr => attr.name == \"kyc_status\" && attr.value == \"verified\")",
description = "Check if target user has verified KYC status"
),
AbacRuleExampleJsonV600(
category = "User Attributes - Target User",
title = "Check User Tier Level",
code = "userAttributes.find(_.name == \"tier\").exists(_.value.toInt >= 2)",
description = "Check if user tier is 2 or higher"
),
AbacRuleExampleJsonV600(
category = "Bank",
title = "Check Specific Bank ID",
code = "bankOpt.isDefined && bankOpt.get.bankId.value == \"gh.29.uk\"",
description = "Check if bank context is defined and matches specific bank ID"
),
AbacRuleExampleJsonV600(
category = "Bank",
title = "Check Bank Name Contains Text",
code = "bankOpt.exists(_.fullName.contains(\"Community\"))",
description = "Check if bank full name contains specific text"
),
AbacRuleExampleJsonV600(
category = "Bank",
title = "Check Bank Has HTTPS Website",
code = "bankOpt.exists(_.websiteUrl.contains(\"https://\"))",
description = "Check if bank website uses HTTPS"
),
AbacRuleExampleJsonV600(
category = "Bank Attributes",
title = "Check Bank Region",
code = "bankAttributes.exists(attr => attr.name == \"region\" && attr.value == \"EU\")",
description = "Check if bank is in EU region"
),
AbacRuleExampleJsonV600(
category = "Bank Attributes",
title = "Check Bank Certification",
code = "bankAttributes.exists(attr => attr.name == \"certified\" && attr.value == \"true\")",
description = "Check if bank has certification attribute"
),
AbacRuleExampleJsonV600(
category = "Account",
title = "Check Minimum Balance",
code = "accountOpt.isDefined && accountOpt.get.balance > 1000",
description = "Check if account balance is above threshold"
),
AbacRuleExampleJsonV600(
category = "Account",
title = "Check USD Account Balance",
code = "accountOpt.exists(acc => acc.currency == \"USD\" && acc.balance > 5000)",
description = "Check if USD account has balance above $5000"
),
AbacRuleExampleJsonV600(
category = "Account",
title = "Check Account Type",
code = "accountOpt.exists(_.accountType == \"SAVINGS\")",
description = "Check if account is a savings account"
),
AbacRuleExampleJsonV600(
category = "Account",
title = "Check Account Number Length",
code = "accountOpt.exists(_.number.length >= 10)",
description = "Check if account number has minimum length"
),
AbacRuleExampleJsonV600(
category = "Account Attributes",
title = "Check Account Status",
code = "accountAttributes.exists(attr => attr.name == \"status\" && attr.value == \"active\")",
description = "Check if account has active status"
),
AbacRuleExampleJsonV600(
category = "Account Attributes",
title = "Check Account Tier",
code = "accountAttributes.exists(attr => attr.name == \"account_tier\" && attr.value == \"gold\")",
description = "Check if account has gold tier"
),
AbacRuleExampleJsonV600(
category = "Transaction",
title = "Check Transaction Amount Limit",
code = "transactionOpt.isDefined && transactionOpt.get.amount < 10000",
description = "Check if transaction amount is below limit"
),
AbacRuleExampleJsonV600(
category = "Transaction",
title = "Check Transaction Type",
code = "transactionOpt.exists(_.transactionType.contains(\"TRANSFER\"))",
description = "Check if transaction is a transfer type"
),
AbacRuleExampleJsonV600(
category = "Transaction",
title = "Check EUR Transaction Amount",
code = "transactionOpt.exists(t => t.currency == \"EUR\" && t.amount > 100)",
description = "Check if EUR transaction exceeds €100"
),
AbacRuleExampleJsonV600(
category = "Transaction",
title = "Check Positive Balance After Transaction",
code = "transactionOpt.exists(_.balance > 0)",
description = "Check if balance remains positive after transaction"
),
AbacRuleExampleJsonV600(
category = "Transaction Attributes",
title = "Check Transaction Category",
code = "transactionAttributes.exists(attr => attr.name == \"category\" && attr.value == \"business\")",
description = "Check if transaction is categorized as business"
),
AbacRuleExampleJsonV600(
category = "Transaction Attributes",
title = "Check Transaction Not Flagged",
code = "!transactionAttributes.exists(attr => attr.name == \"flagged\" && attr.value == \"true\")",
description = "Check that transaction is not flagged"
),
AbacRuleExampleJsonV600(
category = "Transaction Request",
title = "Check Pending Status",
code = "transactionRequestOpt.exists(_.status == \"PENDING\")",
description = "Check if transaction request is pending"
),
AbacRuleExampleJsonV600(
category = "Transaction Request",
title = "Check SEPA Type",
code = "transactionRequestOpt.exists(_.type == \"SEPA\")",
description = "Check if transaction request is SEPA type"
),
AbacRuleExampleJsonV600(
category = "Transaction Request",
title = "Check Same Bank",
code = "transactionRequestOpt.exists(_.this_bank_id.value == bankOpt.get.bankId.value)",
description = "Check if transaction request is for the same bank (unsafe - use exists)"
),
AbacRuleExampleJsonV600(
category = "Transaction Request Attributes",
title = "Check High Priority",
code = "transactionRequestAttributes.exists(attr => attr.name == \"priority\" && attr.value == \"high\")",
description = "Check if transaction request has high priority"
),
AbacRuleExampleJsonV600(
category = "Transaction Request Attributes",
title = "Check Mobile App Source",
code = "transactionRequestAttributes.exists(attr => attr.name == \"source\" && attr.value == \"mobile_app\")",
description = "Check if transaction request originated from mobile app"
),
AbacRuleExampleJsonV600(
category = "Customer",
title = "Check Corporate Customer",
code = "customerOpt.exists(_.legalName.contains(\"Corp\"))",
description = "Check if customer legal name contains Corp"
),
AbacRuleExampleJsonV600(
category = "Customer",
title = "Check Customer Email Matches User",
code = "customerOpt.isDefined && customerOpt.get.email == authenticatedUser.emailAddress",
description = "Check if customer email matches authenticated user"
),
AbacRuleExampleJsonV600(
category = "Customer",
title = "Check Active Customer Relationship",
code = "customerOpt.exists(_.relationshipStatus == \"ACTIVE\")",
description = "Check if customer relationship is active"
),
AbacRuleExampleJsonV600(
category = "Customer",
title = "Check Customer Has Mobile",
code = "customerOpt.exists(_.mobileNumber.nonEmpty)",
description = "Check if customer has mobile number on file"
),
AbacRuleExampleJsonV600(
category = "Customer Attributes",
title = "Check Low Risk Customer",
code = "customerAttributes.exists(attr => attr.name == \"risk_level\" && attr.value == \"low\")",
description = "Check if customer has low risk level"
),
AbacRuleExampleJsonV600(
category = "Customer Attributes",
title = "Check VIP Status",
code = "customerAttributes.exists(attr => attr.name == \"vip_status\" && attr.value == \"true\")",
description = "Check if customer has VIP status"
),
AbacRuleExampleJsonV600(
category = "Call Context",
title = "Check Internal Network",
code = "callContext.exists(_.ipAddress.exists(_.startsWith(\"192.168\")))",
description = "Check if request comes from internal network"
),
AbacRuleExampleJsonV600(
category = "Call Context",
title = "Check GET Request",
code = "callContext.exists(_.verb.exists(_ == \"GET\"))",
description = "Check if request is GET method"
),
AbacRuleExampleJsonV600(
category = "Call Context",
title = "Check URL Contains Pattern",
code = "callContext.exists(_.url.exists(_.contains(\"/accounts/\")))",
description = "Check if request URL contains accounts path"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - User Comparisons",
title = "Self Access Check",
code = "userOpt.exists(_.userId == authenticatedUser.userId)",
description = "Check if target user is the authenticated user"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - User Comparisons",
title = "Same Email Check",
code = "userOpt.exists(_.emailAddress == authenticatedUser.emailAddress)",
description = "Check if target user has same email as authenticated user"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - User Comparisons",
title = "Same Email Domain",
code = "userOpt.exists(u => authenticatedUser.emailAddress.split(\"@\")(1) == u.emailAddress.split(\"@\")(1))",
description = "Check if both users belong to same email domain"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - User Comparisons",
title = "Delegation Match",
code = "onBehalfOfUserOpt.isDefined && userOpt.isDefined && onBehalfOfUserOpt.get.userId == userOpt.get.userId",
description = "Check if delegation user matches target user"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - User Comparisons",
title = "Different User Access",
code = "userOpt.exists(_.userId != authenticatedUser.userId)",
description = "Check if accessing a different user's data"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Customer Comparisons",
title = "Customer Email Matches Auth User",
code = "customerOpt.exists(_.email == authenticatedUser.emailAddress)",
description = "Check if customer email matches authenticated user"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Customer Comparisons",
title = "Customer Email Matches Target User",
code = "customerOpt.isDefined && userOpt.isDefined && customerOpt.get.email == userOpt.get.emailAddress",
description = "Check if customer email matches target user email"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Customer Comparisons",
title = "Customer Name Contains User Name",
code = "customerOpt.exists(c => userOpt.exists(u => c.legalName.contains(u.name)))",
description = "Check if customer legal name contains user name"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Account/Transaction",
title = "Transaction Within Balance",
code = "transactionOpt.isDefined && accountOpt.isDefined && transactionOpt.get.amount < accountOpt.get.balance",
description = "Check if transaction amount is less than account balance"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Account/Transaction",
title = "Transaction Within 50% Balance",
code = "transactionOpt.exists(t => accountOpt.exists(a => t.amount <= a.balance * 0.5))",
description = "Check if transaction is within 50% of account balance"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Account/Transaction",
title = "Same Currency Check",
code = "transactionOpt.exists(t => accountOpt.exists(a => t.currency == a.currency))",
description = "Check if transaction and account have same currency"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Account/Transaction",
title = "Sufficient Funds After Transaction",
code = "transactionOpt.exists(t => accountOpt.exists(a => a.balance - t.amount >= 0))",
description = "Check if account will have sufficient funds after transaction"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Account/Transaction",
title = "Debit from Checking",
code = "transactionOpt.exists(t => accountOpt.exists(a => (a.accountType == \"CHECKING\" && t.transactionType.exists(_.contains(\"DEBIT\")))))",
description = "Check if debit transaction from checking account"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Bank/Account",
title = "Account Belongs to Bank",
code = "accountOpt.isDefined && bankOpt.isDefined && accountOpt.get.bankId == bankOpt.get.bankId.value",
description = "Check if account belongs to the bank"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Bank/Account",
title = "Account Currency Matches Bank Currency",
code = "accountOpt.exists(a => bankAttributes.exists(attr => attr.name == \"primary_currency\" && attr.value == a.currency))",
description = "Check if account currency matches bank's primary currency"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Transaction Request",
title = "Transaction Request for Account",
code = "transactionRequestOpt.exists(tr => accountOpt.exists(a => tr.this_account_id.value == a.accountId.value))",
description = "Check if transaction request is for this account"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Transaction Request",
title = "Transaction Request for Bank",
code = "transactionRequestOpt.exists(tr => bankOpt.exists(b => tr.this_bank_id.value == b.bankId.value))",
description = "Check if transaction request is for this bank"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Transaction Request",
title = "Transaction Amount Matches Charge",
code = "transactionOpt.isDefined && transactionRequestOpt.isDefined && transactionOpt.get.amount == transactionRequestOpt.get.charge.value.toDouble",
description = "Check if transaction amount matches request charge"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Attribute Comparisons",
title = "User and Account Same Tier",
code = "userAttributes.exists(ua => ua.name == \"tier\" && accountAttributes.exists(aa => aa.name == \"tier\" && ua.value == aa.value))",
description = "Check if user tier matches account tier"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Attribute Comparisons",
title = "Customer and Account Same Segment",
code = "customerAttributes.exists(ca => ca.name == \"segment\" && accountAttributes.exists(aa => aa.name == \"segment\" && ca.value == aa.value))",
description = "Check if customer segment matches account segment"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Attribute Comparisons",
title = "Auth User and Account Same Department",
code = "authenticatedUserAttributes.exists(ua => ua.name == \"department\" && accountAttributes.exists(aa => aa.name == \"department\" && ua.value == aa.value))",
description = "Check if authenticated user department matches account department"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Attribute Comparisons",
title = "Transaction Risk Within User Tolerance",
code = "transactionAttributes.exists(ta => ta.name == \"risk_score\" && userAttributes.exists(ua => ua.name == \"risk_tolerance\" && ta.value.toInt <= ua.value.toInt))",
description = "Check if transaction risk is within user's tolerance"
),
AbacRuleExampleJsonV600(
category = "Cross-Object - Attribute Comparisons",
title = "Bank and Customer Same Region",
code = "bankAttributes.exists(ba => ba.name == \"region\" && customerAttributes.exists(ca => ca.name == \"region\" && ba.value == ca.value))",
description = "Check if bank and customer are in same region"
),
AbacRuleExampleJsonV600(
category = "Complex - Multi-Object",
title = "Bank Employee with Active Account",
code = "authenticatedUser.emailAddress.endsWith(\"@bank.com\") && accountOpt.exists(_.balance > 0) && bankOpt.exists(_.bankId.value == \"gh.29.uk\")",
description = "Check if bank employee accessing active account at specific bank"
),
AbacRuleExampleJsonV600(
category = "Complex - Multi-Object",
title = "Manager Accessing Other User",
code = "authenticatedUserAttributes.exists(_.name == \"role\" && _.value == \"manager\") && userOpt.exists(_.userId != authenticatedUser.userId)",
description = "Check if manager is accessing another user's data"
),
AbacRuleExampleJsonV600(
category = "Complex - Multi-Object",
title = "Self or Authorized Delegation with Balance",
code = "(onBehalfOfUserOpt.isEmpty || onBehalfOfUserOpt.get.userId == authenticatedUser.userId) && accountOpt.exists(_.balance > 1000)",
description = "Check if self-access or authorized delegation with minimum balance"
),
AbacRuleExampleJsonV600(
category = "Complex - Multi-Object",
title = "Verified User with Optional Delegation",
code = "userAttributes.exists(_.name == \"kyc_status\" && _.value == \"verified\") && (onBehalfOfUserOpt.isEmpty || onBehalfOfUserAttributes.exists(_.name == \"authorized\"))",
description = "Check if user is KYC verified and delegation is authorized (if present)"
),
AbacRuleExampleJsonV600(
category = "Complex - Multi-Object",
title = "VIP Customer with Premium Account",
code = "customerAttributes.exists(_.name == \"vip_status\" && _.value == \"true\") && accountAttributes.exists(_.name == \"account_tier\" && _.value == \"premium\")",
description = "Check if VIP customer has premium account"
),
AbacRuleExampleJsonV600(
category = "Complex - Chained Validation",
title = "User-Customer-Account-Transaction Chain",
code = "userOpt.exists(u => customerOpt.exists(c => c.email == u.emailAddress && accountOpt.exists(a => transactionOpt.exists(t => t.accountId.value == a.accountId.value))))",
description = "Validate complete chain from user to customer to account to transaction"
),
AbacRuleExampleJsonV600(
category = "Complex - Chained Validation",
title = "Bank-Account-Transaction Request Chain",
code = "bankOpt.exists(b => accountOpt.exists(a => a.bankId == b.bankId.value && transactionRequestOpt.exists(tr => tr.this_account_id.value == a.accountId.value)))",
description = "Validate bank owns account and transaction request is for that account"
),
AbacRuleExampleJsonV600(
category = "Complex - Aggregation",
title = "Matching Attributes",
code = "authenticatedUserAttributes.exists(aua => userAttributes.exists(ua => aua.name == ua.name && aua.value == ua.value))",
description = "Check if authenticated user and target user share any matching attributes"
),
AbacRuleExampleJsonV600(
category = "Complex - Aggregation",
title = "Allowed Transaction Attributes",
code = "transactionAttributes.forall(ta => accountAttributes.exists(aa => aa.name == \"allowed_transaction_\" + ta.name))",
description = "Check if all transaction attributes are allowed for this account"
),
AbacRuleExampleJsonV600(
category = "Business Logic - Loan Approval",
title = "Credit Score and Balance Check",
code = "customerAttributes.exists(ca => ca.name == \"credit_score\" && ca.value.toInt > 650) && accountOpt.exists(_.balance > 5000)",
description = "Check if customer has good credit score and sufficient balance for loan"
),
AbacRuleExampleJsonV600(
category = "Business Logic - Wire Transfer",
title = "Wire Transfer Authorization",
code = "transactionOpt.exists(t => t.amount < 100000 && t.transactionType.exists(_.contains(\"WIRE\"))) && authenticatedUserAttributes.exists(_.name == \"wire_authorized\")",
description = "Check if wire transfer is under limit and user is authorized"
),
AbacRuleExampleJsonV600(
category = "Business Logic - Account Closure",
title = "Self-Service or Manager Account Closure",
code = "accountOpt.exists(a => (a.balance == 0 && userOpt.exists(_.userId == authenticatedUser.userId)) || authenticatedUserAttributes.exists(_.name == \"role\" && _.value == \"manager\"))",
description = "Allow account closure if zero balance self-service or manager override"
),
AbacRuleExampleJsonV600(
category = "Business Logic - VIP Processing",
title = "VIP Priority Check",
code = "(customerAttributes.exists(_.name == \"vip_status\" && _.value == \"true\") || accountAttributes.exists(_.name == \"account_tier\" && _.value == \"platinum\"))",
description = "Check if customer or account qualifies for VIP priority processing"
),
AbacRuleExampleJsonV600(
category = "Business Logic - Joint Account",
title = "Joint Account Access",
code = "accountOpt.exists(a => a.accountHolders.exists(h => h.userId == authenticatedUser.userId || h.emailAddress == authenticatedUser.emailAddress))",
description = "Check if authenticated user is one of the account holders"
rule_name = "Fraud Analyst High-Risk Transaction Access",
rule_code = "authenticatedUserEntitlements.exists(e => e.roleName == \"CanReadTransactionsAtOneBank\") && callContext.exists(c => c.verb.exists(_ == \"GET\") && c.implementedByPartialFunction.exists(_.contains(\"Transaction\"))) && transactionAttributes.exists(ta => ta.name == \"risk_score\" && ta.value.toInt >= 75) && transactionOpt.exists(_.status.exists(_ != \"COMPLETED\"))",
description = "Allow users with CanReadTransactionsAtOneBank role to GET high-risk (score ≥75) non-completed transactions",
policy = "transaction-access",
is_active = true
)
),
available_operators = List(
@ -5795,6 +5420,59 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
getAbacPolicies,
implementedInApiVersion,
nameOf(getAbacPolicies),
"GET",
"/management/abac-policies",
"Get ABAC Policies",
s"""Get the list of allowed ABAC policy names.
|
|ABAC rules are organized by policies. Each rule must have at least one policy assigned.
|Rules can have multiple policies (comma-separated). This endpoint returns the list of
|standardized policy names that should be used when creating or updating rules.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
AbacPoliciesJsonV600(
policies = List(
AbacPolicyJsonV600(
policy = "account-access",
description = "Rules for controlling access to account information"
)
)
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagABAC),
Some(List(canGetAbacRule))
)
lazy val getAbacPolicies: OBPEndpoint = {
case "management" :: "abac-policies" :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canGetAbacRule, callContext)
} yield {
val policies = Constant.ABAC_POLICIES.map { policy =>
AbacPolicyJsonV600(
policy = policy,
description = Constant.ABAC_POLICY_DESCRIPTIONS.getOrElse(policy, "No description available")
)
}
(AbacPoliciesJsonV600(policies), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
validateAbacRule,
implementedInApiVersion,
@ -5992,6 +5670,112 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
executeAbacPolicy,
implementedInApiVersion,
nameOf(executeAbacPolicy),
"POST",
"/management/abac-policies/POLICY/execute",
"Execute ABAC Policy",
s"""Execute all ABAC rules in a policy to test access control.
|
|This endpoint executes all active rules that belong to the specified policy.
|The policy uses OR logic - access is granted if at least one rule passes.
|
|This allows you to test a complete policy with specific context (authenticated user, bank, account, transaction, customer, etc.).
|
|**Documentation:**
|- ${Glossary.getGlossaryItemLink("ABAC_Simple_Guide")} - Getting started with ABAC rules
|- ${Glossary.getGlossaryItemLink("ABAC_Parameters_Summary")} - Complete list of all 18 parameters
|- ${Glossary.getGlossaryItemLink("ABAC_Object_Properties_Reference")} - Detailed property reference
|- ${Glossary.getGlossaryItemLink("ABAC_Testing_Examples")} - Testing examples and patterns
|
|You can provide optional IDs in the request body to test the policy with specific context.
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
ExecuteAbacRuleJsonV600(
authenticated_user_id = Some("c7b6cb47-cb96-4441-8801-35b57456753a"),
on_behalf_of_user_id = Some("a3b5c123-1234-5678-9012-fedcba987654"),
user_id = Some("c7b6cb47-cb96-4441-8801-35b57456753a"),
bank_id = Some("gh.29.uk"),
account_id = Some("8ca8a7e4-6d02-48e3-a029-0b2bf89de9f0"),
view_id = Some("owner"),
transaction_request_id = Some("123456"),
transaction_id = Some("abc123"),
customer_id = Some("customer-id-123")
),
AbacRuleResultJsonV600(
result = true
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
InvalidJsonFormat,
UnknownError
),
List(apiTagABAC),
Some(List(canExecuteAbacRule))
)
lazy val executeAbacPolicy: OBPEndpoint = {
case "management" :: "abac-policies" :: policy :: "execute" :: Nil JsonPost json -> _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(user), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", user.userId, canExecuteAbacRule, callContext)
execJson <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, callContext) {
json.extract[ExecuteAbacRuleJsonV600]
}
// Verify the policy exists
_ <- Future {
if (Constant.ABAC_POLICIES.contains(policy)) {
Full(true)
} else {
Failure(s"Policy not found: $policy. Available policies: ${Constant.ABAC_POLICIES.mkString(", ")}")
}
} map {
unboxFullOrFail(_, callContext, s"Invalid ABAC Policy: $policy", 404)
}
// Execute the policy with IDs - object fetching happens internally
// authenticatedUserId: can be provided in request (for testing) or defaults to actual authenticated user
// onBehalfOfUserId: optional delegation - acting on behalf of another user
// userId: the target user being evaluated (defaults to authenticated user)
effectiveAuthenticatedUserId = execJson.authenticated_user_id.getOrElse(user.userId)
result <- Future {
val resultBox = AbacRuleEngine.executeRulesByPolicy(
policy = policy,
authenticatedUserId = effectiveAuthenticatedUserId,
onBehalfOfUserId = execJson.on_behalf_of_user_id,
userId = execJson.user_id,
callContext = callContext.getOrElse(cc),
bankId = execJson.bank_id,
accountId = execJson.account_id,
viewId = execJson.view_id,
transactionId = execJson.transaction_id,
transactionRequestId = execJson.transaction_request_id,
customerId = execJson.customer_id
)
resultBox match {
case Full(allowed) =>
AbacRuleResultJsonV600(result = allowed)
case Failure(msg, _, _) =>
AbacRuleResultJsonV600(result = false)
case Empty =>
AbacRuleResultJsonV600(result = false)
}
}
} yield {
(result, HttpCode.`200`(callContext))
}
}
}
// ============================================================================================================
// USER ATTRIBUTES v6.0.0 - Consistent with other entity attributes
// ============================================================================================================

View File

@ -380,6 +380,7 @@ case class CreateAbacRuleJsonV600(
rule_name: String,
rule_code: String,
description: String,
policy: String,
is_active: Boolean
)
@ -387,6 +388,7 @@ case class UpdateAbacRuleJsonV600(
rule_name: String,
rule_code: String,
description: String,
policy: String,
is_active: Boolean
)
@ -396,6 +398,7 @@ case class AbacRuleJsonV600(
rule_code: String,
is_active: Boolean,
description: String,
policy: String,
created_by_user_id: String,
updated_by_user_id: String
)
@ -459,10 +462,11 @@ case class AbacObjectTypeJsonV600(
)
case class AbacRuleExampleJsonV600(
category: String,
title: String,
code: String,
description: String
rule_name: String,
rule_code: String,
description: String,
policy: String,
is_active: Boolean
)
case class AbacRuleSchemaJsonV600(
@ -473,6 +477,15 @@ case class AbacRuleSchemaJsonV600(
notes: List[String]
)
case class AbacPolicyJsonV600(
policy: String,
description: String
)
case class AbacPoliciesJsonV600(
policies: List[AbacPolicyJsonV600]
)
object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
def createRedisCallCountersJson(
@ -1086,6 +1099,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
rule_code = rule.ruleCode,
is_active = rule.isActive,
description = rule.description,
policy = rule.policy,
created_by_user_id = rule.createdByUserId,
updated_by_user_id = rule.updatedByUserId
)