diff --git a/.github/workflows/auto_update_base_image.yml b/.github/workflows/auto_update_base_image.yml deleted file mode 100644 index 3048faf15..000000000 --- a/.github/workflows/auto_update_base_image.yml +++ /dev/null @@ -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' diff --git a/.github/workflows/build_container_develop_branch.yml b/.github/workflows/build_container_develop_branch.yml index 3afc3d6ec..db6bd5160 100644 --- a/.github/workflows/build_container_develop_branch.yml +++ b/.github/workflows/build_container_develop_branch.yml @@ -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}}" - - diff --git a/.github/workflows/build_container_non_develop_branch.yml b/.github/workflows/build_container_non_develop_branch.yml index fda13bb72..e69de29bb 100644 --- a/.github/workflows/build_container_non_develop_branch.yml +++ b/.github/workflows/build_container_non_develop_branch.yml @@ -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}}" - - diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 61d1e05a5..e69de29bb 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -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/ - - - diff --git a/.github/workflows/run_trivy.yml b/.github/workflows/run_trivy.yml deleted file mode 100644 index 4636bd311..000000000 --- a/.github/workflows/run_trivy.yml +++ /dev/null @@ -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' \ No newline at end of file diff --git a/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala b/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala index 5b531af98..93fb81537 100644 --- a/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala +++ b/obp-api/src/main/scala/code/abacrule/AbacRuleEngine.scala @@ -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 * diff --git a/obp-api/src/main/scala/code/abacrule/AbacRuleTrait.scala b/obp-api/src/main/scala/code/abacrule/AbacRuleTrait.scala index e4309f342..9e9a22885 100644 --- a/obp-api/src/main/scala/code/abacrule/AbacRuleTrait.scala +++ b/obp-api/src/main/scala/code/abacrule/AbacRuleTrait.scala @@ -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() diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index cd2d2da17..b355e782e 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -1257,3 +1257,4 @@ so the caller must specify any required filtering by catalog explicitly. } + diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index 73cee00a6..9816ad4a2 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -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" diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index e8e9956b9..c1a47283b 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -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! diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 9c7a990be..9e1f404b7 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -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() diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index bd4c41f01..38208d32d 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -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") diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index 0141b3d58..9a31b35fb 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -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 _ => { diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index ae885e95f..6331f2a44 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -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 // ============================================================================================================ diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 36ab2d96b..55a92ef0f 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -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 )