diff --git a/.github/workflows/build_container_develop_branch.yml b/.github/workflows/build_container_develop_branch.yml index 211cc9cb9..c9355f523 100644 --- a/.github/workflows/build_container_develop_branch.yml +++ b/.github/workflows/build_container_develop_branch.yml @@ -3,12 +3,9 @@ name: Build and publish container develop # read-write repo token # access to secrets on: - workflow_run: - workflows: [build maven artifact] + push: branches: - develop - types: - - completed env: ## Sets environment variable @@ -19,35 +16,78 @@ env: jobs: build: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} + 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@v3 - - name: 'Download artifact' - uses: actions/github-script@v7 + - uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 with: - script: | - var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "push" - })[0]; - var download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/push.zip', Buffer.from(download.data)); - - run: unzip push.zip - - - name: prepare the artifact + java-version: '11' + distribution: 'adopt' + cache: maven + - name: Build with Maven run: | - mkdir -p obp-api/target/ - cp obp-api-1.10.1.war obp-api/target/obp-api-1.10.1.war + 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 kafka.akka.timeout = 9 >> obp-api/src/main/resources/props/test.default.props + echo remotedata.timeout = 10 >> 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 + + - 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: | diff --git a/.github/workflows/build_container_non_develop_branch.yml b/.github/workflows/build_container_non_develop_branch.yml index 7c8a0e695..0a3a1e608 100644 --- a/.github/workflows/build_container_non_develop_branch.yml +++ b/.github/workflows/build_container_non_develop_branch.yml @@ -1,54 +1,93 @@ name: Build and publish container non develop -# read-write repo token -# access to secrets on: - workflow_run: - workflows: [build maven artifact] + push: branches: - '*' - '!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' }} + 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@v3 - - name: 'Download artifact' - uses: actions/github-script@v7 + - 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@v2 with: - script: | - var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "push" - })[0]; - var download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/push.zip', Buffer.from(download.data)); - - run: unzip push.zip - - - name: prepare the artifact + java-version: '11' + distribution: 'adopt' + cache: maven + - name: Build with Maven run: | - mkdir -p obp-api/target/ - cp obp-api-1.10.1.war obp-api/target/obp-api-1.10.1.war + 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 kafka.akka.timeout = 9 >> obp-api/src/main/resources/props/test.default.props + echo remotedata.timeout = 10 >> 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 + + - 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: | diff --git a/.github/workflows/build_contributer_container.yml b/.github/workflows/build_contributer_container.yml deleted file mode 100644 index 5700bbdb2..000000000 --- a/.github/workflows/build_contributer_container.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build and publish commiter container - -# read-write repo token -# access to secrets -on: - workflow_run: - workflows: [Build on Pull Request] - types: - - completed - -env: - ## Sets environment variable - DOCKER_HUB_ORGANIZATION: ${{ vars.DOCKER_HUB_ORGANIZATION }} - -jobs: - upload: - runs-on: ubuntu-latest - if: > - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.conclusion == 'success' - steps: - - uses: actions/checkout@v3 - - name: 'Download artifact' - uses: actions/github-script@v7 - with: - script: | - var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "pr" - })[0]; - var download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data)); - - run: unzip pr.zip - - name: Get user from file - run: echo "USER_NAME=$(cat UN)" >> $GITHUB_ENV - - - name: prepare the artifact - run: | - mkdir -p obp-api/target/ - cp obp-api-1.10.1.war obp-api/target/obp-api-1.10.1.war - - - 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 }}/obp-api-${{ env.USER_NAME }}:${{ github.event.pull_request.head }} --tag docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }}:latest - docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }} --all-tags - echo docker done diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml deleted file mode 100644 index e1d246800..000000000 --- a/.github/workflows/build_package.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: build maven artifact - -on: [push] - -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@v3 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - cache: maven - - name: Build with Maven - run: | - 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 - - - name: Save .war artifact - run: | - mkdir -p ./push - cp obp-api/target/obp-api-1.*.war ./push/ - - uses: actions/upload-artifact@v4 - with: - name: push - path: push/ - - - - - - diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index d47c47fcd..24500a23b 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -27,9 +27,9 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'adopt' @@ -73,15 +73,12 @@ jobs: echo consents.allowed=true >> obp-api/src/main/resources/props/test.default.props MAVEN_OPTS="-Xmx3G -Xss2m" mvn clean package -Pprod - - name: Save user name and .war artifact + - name: Build the Docker image run: | - mkdir -p ./pr - echo ${{ github.event.pull_request.user.login }} > ./pr/UN - cp obp-api/target/obp-api-1.10.1.war ./pr/obp-api-1.10.1.war - - uses: actions/upload-artifact@v4 - with: - name: pr - path: pr/ + 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 }}/obp-api-${{ env.USER_NAME }}:latest + docker push docker.io/${{ env.DOCKER_HUB_ORGANIZATION }}/obp-api-${{ env.USER_NAME }} --all-tags + echo docker done diff --git a/.github/workflows/run_trivy.yml b/.github/workflows/run_trivy.yml index 548cd92ad..4636bd311 100644 --- a/.github/workflows/run_trivy.yml +++ b/.github/workflows/run_trivy.yml @@ -19,7 +19,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: trivy-db name: Check trivy db sha env: @@ -31,7 +31,7 @@ jobs: sha=$(gh api -H "${headers}" "${endpoint}" | jq --raw-output "${jqFilter}") echo "Trivy DB sha256:${sha}" echo "::set-output name=sha::${sha}" - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: .trivy key: ${{ runner.os }}-trivy-db-${{ steps.trivy-db.outputs.sha }} @@ -49,6 +49,6 @@ jobs: - 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@v1 + 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/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index fd10c7472..a3ecc27fd 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -157,6 +157,12 @@ jwt.use.ssl=false # truststore.password.redis = truststore-password +## Trust stores is a list of trusted CA certificates +## Public certificate for the CA (used by clients and servers to validate signatures) +# truststore.path.tpp_signature = path/to/ca.p12 +# truststore.password.tpp_signature = truststore-password + + ## Enable writing API metrics (which APIs are called) to RDBMS write_metrics=true ## Enable writing connector metrics (which methods are called)to RDBMS diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index 5040e26b9..ccc0db137 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -311,6 +311,8 @@ of the PSU at this ASPSP. (Full(u), callContext) <- authenticatedAccess(cc) _ <- passesPsd2Aisp(callContext) (availablePrivateAccounts, callContext) <- NewStyle.function.getAccountListOfBerlinGroup(u, callContext) + (canReadBalancesAccounts, callContext) <- NewStyle.function.getAccountCanReadBalancesOfBerlinGroup(u, callContext) + (canReadTransactionsAccounts, callContext) <- NewStyle.function.getAccountCanReadTransactionsOfBerlinGroup(u, callContext) (accounts, callContext) <- NewStyle.function.getBankAccounts(availablePrivateAccounts, callContext) bankAccountsFiltered = accounts.filter(bankAccount => bankAccount.attributes.toList.flatten.find(attribute => @@ -320,7 +322,12 @@ of the PSU at this ASPSP. ).isEmpty) } yield { - (JSONFactory_BERLIN_GROUP_1_3.createAccountListJson(bankAccountsFiltered, u), callContext) + (JSONFactory_BERLIN_GROUP_1_3.createAccountListJson( + bankAccountsFiltered, + canReadBalancesAccounts, + canReadTransactionsAccounts, + u + ), callContext) } } } diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 7b59df4c9..8cefe014f 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -49,8 +49,8 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { ) case class CoreAccountLinksJsonV13( - balances: LinkHrefJson //, -// trasactions: LinkHrefJson // These links are only supported, when the corresponding consent has been already granted. + balances: LinkHrefJson, + transactions: Option[LinkHrefJson] = None // These links are only supported, when the corresponding consent has been already granted. ) case class CoreAccountBalancesJson( @@ -69,7 +69,6 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { product: String, cashAccountType: String, // status: String="enabled", - bic: String, // linkedAccounts: String ="string", // usage: String ="PRIV", // details: String ="", @@ -308,10 +307,17 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { ) - def createAccountListJson(bankAccounts: List[BankAccount], user: User): CoreAccountsJsonV13 = { + def createAccountListJson(bankAccounts: List[BankAccount], + canReadBalancesAccounts: List[BankIdAccountId], + canReadTransactionsAccounts: List[BankIdAccountId], + user: User): CoreAccountsJsonV13 = { CoreAccountsJsonV13(bankAccounts.map { x => val (iBan: String, bBan: String) = getIbanAndBban(x) + val commonPath = s"${OBP_BERLIN_GROUP_1_3.apiVersion.urlPrefix}/${OBP_BERLIN_GROUP_1_3.version}/accounts/${x.accountId.value}" + val balanceRef = LinkHrefJson(s"/$commonPath/accounts/${x.accountId.value}/balances") + val transactionRef = LinkHrefJson(s"/$commonPath/accounts/${x.accountId.value}/transactions") + val canReadTransactions = canReadTransactionsAccounts.map(_.accountId.value).contains(x.accountId.value) CoreAccountJsonV13( @@ -320,10 +326,12 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { bban = bBan, currency = x.currency, name = x.name, - bic = getBicFromBankId(x.bankId.value), cashAccountType = x.accountType, product = x.accountType, - _links = CoreAccountLinksJsonV13(LinkHrefJson(s"/${OBP_BERLIN_GROUP_1_3.apiVersion.urlPrefix}/${OBP_BERLIN_GROUP_1_3.version}/accounts/${x.accountId.value}/balances")) + _links = CoreAccountLinksJsonV13( + balances = balanceRef, + transactions = if(canReadTransactions) Some(transactionRef) else None, + ) ) } ) @@ -340,7 +348,6 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { bban = bBan, currency = x.currency, name = x.name, - bic = getBicFromBankId(x.bankId.value), cashAccountType = x.accountType, product = x.accountType, _links = CoreAccountLinksJsonV13(LinkHrefJson(s"/${OBP_BERLIN_GROUP_1_3.apiVersion.urlPrefix}/${OBP_BERLIN_GROUP_1_3.version}/accounts/${x.accountId.value}/balances")) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 4c5412125..ed5d6856c 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -61,7 +61,7 @@ object Redis extends MdcLoggable { // Load the CA certificate val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis") + val trustStorePassword = APIUtil.getPropsValue("truststore.password.redis") .getOrElse(APIUtil.initPasswd).toCharArray val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("") val trustStoreStream = new FileInputStream(truststorePath) diff --git a/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json b/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json index 640c91a4d..f6ee011d8 100644 --- a/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json +++ b/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json @@ -12,47 +12,48 @@ "logo":"https://static.openbankproject.com/images/sandbox/bank_y.png", "website":"https://www.example.com" }], - "users":[{ - "email":"robert.xuk.x@example.com", - "user_name": "robert.xuk.x@example.com", - "password":"5232e7", - "display_name":"Robert XUk X" - },{ - "email":"susan.xuk.x@example.com", - "user_name": "susan.xuk.x@example.com", - "password":"43ca4d", - "display_name":"Susan XUk X" - },{ - "email":"anil.xuk.x@example.com", - "user_name": "anil.xuk.x@example.com", - "password":"d8c716", - "display_name":"Anil XUk X" - },{ - "email":"ellie.xuk.x@example.com", - "user_name": "ellie.xuk.x@example.com", - "password":"6187b9", - "display_name":"Ellie XUk X" - },{ - "email":"robert.yuk.y@example.com", - "user_name": "robert.yuk.y@example.com", - "password":"e5046a", - "display_name":"Robert YUk Y" - },{ - "email":"susan.yuk.y@example.com", - "user_name": "susan.yuk.y@example.com", - "password":"5b38a6", - "display_name":"Susan YUk Y" - },{ - "email":"anil.yuk.y@example.com", - "user_name": "anil.yuk.y@example.com", - "password":"dcf03d", - "display_name":"Anil YUk Y" - },{ - "email":"ellie.yuk.y@example.com", - "user_name": "ellie.yuk.y@example.com", - "password":"4f9eaa", - "display_name":"Ellie YUk Y" - }], + "users": [ + { + "email": "robert.x.0.gh@example.com", + "password": "V8%Ktssl(L", + "user_name": "Robert.X.0.GH" + }, + { + "email": "susan.x.0.gh@example.com", + "password": "naW9u3C%bh", + "user_name": "Susan.X.0.GH" + }, + { + "email": "anil.x.0.gh@example.com", + "password": "9W0RIrX-6f", + "user_name": "Anil.X.0.GH" + }, + { + "email": "ellie.x.0.gh@example.com", + "password": "rMf_OHM0dW", + "user_name": "Ellie.X.0.GH" + }, + { + "email": "robert.y.9.gh@example.com", + "password": "%1Z43kzt2L", + "user_name": "Robert.Y.9.GH" + }, + { + "email": "susan.y.9.gh@example.com", + "password": "oITehM!B2V", + "user_name": "Susan.Y.9.GH" + }, + { + "email": "anil.y.9.gh@example.com", + "password": "TuKaNO8oI-", + "user_name": "Anil.Y.9.GH" + }, + { + "email": "ellie.y.9.gh@example.com", + "password": "SkJDH+ds2_", + "user_name": "Ellie.Y.9.GH" + } + ], "accounts":[{ "id":"05237266-b334-4704-a087-5b460a2ecf04", "bank":"psd201-bank-x--uk", diff --git a/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json b/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json index 91418c4d6..0126ddd05 100644 --- a/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json +++ b/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json @@ -12,39 +12,48 @@ "logo":"https://static.openbankproject.com/images/sandbox/bank_y.png", "website":"https://www.example.com" }], - "users":[{ - "email":"robert.x.0.gh@example.com", - "password":"X!d1edcafd", - "user_name":"Robert.X.0.GH" - },{ - "email":"susan.x.0.gh@example.com", - "password":"X!90e4e3e4", - "user_name":"Susan.X.0.GH" - },{ - "email":"anil.x.0.gh@example.com", - "password":"X!eb06b005", - "user_name":"Anil.X.0.GH" - },{ - "email":"ellie.x.0.gh@example.com", - "password":"X!5bc94405", - "user_name":"Ellie.X.0.GH" - },{ - "email":"robert.y.9.gh@example.com", - "password":"X!039941de", - "user_name":"Robert.Y.9.GH" - },{ - "email":"susan.y.9.gh@example.com", - "password":"X!bb4efa3d", - "user_name":"Susan.Y.9.GH" - },{ - "email":"anil.y.9.gh@example.com", - "password":"X!098915cd", - "user_name":"Anil.Y.9.GH" - },{ - "email":"ellie.y.9.gh@example.com", - "password":"X!6170b37b", - "user_name":"Ellie.Y.9.GH" - }], + "users": [ + { + "email": "robert.x.0.gh@example.com", + "password": "V8%Ktssl(L", + "user_name": "Robert.X.0.GH" + }, + { + "email": "susan.x.0.gh@example.com", + "password": "naW9u3C%bh", + "user_name": "Susan.X.0.GH" + }, + { + "email": "anil.x.0.gh@example.com", + "password": "9W0RIrX-6f", + "user_name": "Anil.X.0.GH" + }, + { + "email": "ellie.x.0.gh@example.com", + "password": "rMf_OHM0dW", + "user_name": "Ellie.X.0.GH" + }, + { + "email": "robert.y.9.gh@example.com", + "password": "%1Z43kzt2L", + "user_name": "Robert.Y.9.GH" + }, + { + "email": "susan.y.9.gh@example.com", + "password": "oITehM!B2V", + "user_name": "Susan.Y.9.GH" + }, + { + "email": "anil.y.9.gh@example.com", + "password": "TuKaNO8oI-", + "user_name": "Anil.Y.9.GH" + }, + { + "email": "ellie.y.9.gh@example.com", + "password": "SkJDH+ds2_", + "user_name": "Ellie.Y.9.GH" + } + ], "accounts":[{ "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", "bank":"obp-bank-x-gh", 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 2e789f27d..4d7367dc9 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2353,15 +2353,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } - // Function checks does a consumer specified by a parameter consumerId has at least one role provided by a parameter roles at a bank specified by a parameter bankId - // i.e. does consumer has assigned at least one role from the list - def hasAtLeastOneScope(bankId: String, consumerId: String, roles: List[ApiRole]): Boolean = { - val list: List[Boolean] = for (role <- roles) yield { - !Scope.scope.vend.getScope(if (role.requiresBankId == true) bankId else "", consumerId, role.toString).isEmpty - } - list.exists(_ == true) - } - def hasEntitlement(bankId: String, userId: String, apiRole: ApiRole): Boolean = apiRole match { case RoleCombination(roles) => roles.forall(hasEntitlement(bankId, userId, _)) case role => @@ -3683,7 +3674,12 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } final def checkAuthorisationToCreateTransactionRequest(viewId: ViewId, bankAccountId: BankIdAccountId, user: User, callContext: Option[CallContext]): Box[Boolean] = { - lazy val hasCanCreateAnyTransactionRequestRole = APIUtil.hasEntitlement(bankAccountId.bankId.value, user.userId, canCreateAnyTransactionRequest) + lazy val hasCanCreateAnyTransactionRequestRole = APIUtil.handleEntitlementsAndScopes( + bankAccountId.bankId.value, + user.userId, + APIUtil.getConsumerPrimaryKey(callContext), + List(canCreateAnyTransactionRequest) + ) lazy val view = APIUtil.checkViewAccessAndReturnView(viewId, bankAccountId, Some(user), callContext) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 3cb253db0..c63fc89cc 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -130,7 +130,12 @@ object BerlinGroupSigning { val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders) val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE") val isVerified = verifySignature(signingString, signature, certificatePem) - if (isVerified) forwardResult else (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) + val isValidated = CertificateVerifier.validateCertificate(certificatePem) + (isVerified, isValidated) match { + case (true, true) => forwardResult + case (true, false) => (Failure(ErrorMessages.X509PublicKeyCannotBeValidated), forwardResult._2) + case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) + } case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid case _ => (Failure(ErrorMessages.X509GeneralError), forwardResult._2) // PEM certificate cannot be validated } diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala new file mode 100644 index 000000000..cefb24abe --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -0,0 +1,153 @@ +package code.api.util + +import code.util.Helper.MdcLoggable + +import java.io.{ByteArrayInputStream, FileInputStream} +import java.security.KeyStore +import java.security.cert._ +import java.util.{Base64, Collections} +import javax.net.ssl.TrustManagerFactory +import scala.io.Source +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +object CertificateVerifier extends MdcLoggable { + + /** + * Loads a trust store (`.p12` file) from a configured path. + * + * This function: + * - Reads the trust store password from the application properties (`truststore.path.tpp_signature`). + * - Uses Java's `KeyStore` class to load the certificates. + * + * @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails. + */ + private def loadTrustStore(): Option[KeyStore] = { + val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature") + .or(APIUtil.getPropsValue("truststore.path")).getOrElse("") + val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray + + Try { + val trustStore = KeyStore.getInstance("PKCS12") + val trustStoreInputStream = new FileInputStream(trustStorePath) + try { + trustStore.load(trustStoreInputStream, trustStorePassword) + } finally { + trustStoreInputStream.close() + } + trustStore + } match { + case Success(store) => + logger.info(s"Loaded trust store from: $trustStorePath") + Some(store) + case Failure(e) => + logger.info(s"Failed to load trust store: ${e.getMessage}") + None + } + } + + /** + * Verifies an X.509 certificate against the loaded trust store. + * + * This function: + * - Parses the PEM certificate into an `X509Certificate` using `parsePemToX509Certificate`. + * - Loads the trust store using `loadTrustStore()`. + * - Extracts trusted root CAs from the trust store. + * - Creates PKIX validation parameters and disables revocation checking. + * - Validates the certificate using Java's `CertPathValidator`. + * + * @param pemCertificate The X.509 certificate in PEM format. + * @return `true` if the certificate is valid and trusted, otherwise `false`. + */ + def validateCertificate(pemCertificate: String): Boolean = { + Try { + val certificate = parsePemToX509Certificate(pemCertificate) + + // Load trust store + val trustStore = loadTrustStore() + .getOrElse(throw new Exception("Trust store could not be loaded.")) + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + trustManagerFactory.init(trustStore) + + // Get trusted CAs from the trust store + val trustAnchors = trustStore.aliases().asScala + .filter(trustStore.isCertificateEntry) + .map(alias => trustStore.getCertificate(alias).asInstanceOf[X509Certificate]) + .map(cert => new TrustAnchor(cert, null)) + .toSet + .asJava // Convert Scala Set to Java Set + + if (trustAnchors.isEmpty) throw new Exception("No trusted certificates found in trust store.") + + // Set up PKIX parameters for validation + val pkixParams = new PKIXParameters(trustAnchors) + pkixParams.setRevocationEnabled(false) // Disable CRL checks + + // Validate certificate chain + val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate)) + val validator = CertPathValidator.getInstance("PKIX") + validator.validate(certPath, pkixParams) + + logger.info("Certificate is valid and trusted.") + true + } match { + case Success(_) => true + case Failure(e: CertPathValidatorException) => + logger.info(s"Certificate validation failed: ${e.getMessage}") + false + case Failure(e) => + logger.info(s"Error: ${e.getMessage}") + false + } + } + + /** + * Converts a PEM certificate (Base64-encoded) into an `X509Certificate` object. + * + * This function: + * - Removes the PEM header and footer (`-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`). + * - Decodes the Base64-encoded certificate data. + * - Generates and returns an `X509Certificate` object. + * + * @param pem The X.509 certificate in PEM format. + * @return The parsed `X509Certificate` object. + */ + private def parsePemToX509Certificate(pem: String): X509Certificate = { + val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "") + .replaceAll("-----END CERTIFICATE-----", "") + .replaceAll("\\s", "") + + val decoded = Base64.getDecoder.decode(cleanedPem) + val certFactory = CertificateFactory.getInstance("X.509") + certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate] + } + + def loadPemCertificateFromFile(filePath: String): Option[String] = { + Try { + val source = Source.fromFile(filePath) + try source.getLines().mkString("\n") // Read entire file into a single string + finally source.close() + } match { + case Success(pem) => Some(pem) + case Failure(exception) => + logger.error(s"Failed to load PEM certificate from file: ${exception.getMessage}") + None + } + } + + def main(args: Array[String]): Unit = { + // change the following path if using this function to test on your localhost + val certificatePath = "/path/to/certificate.pem" + val pemCertificate = loadPemCertificateFromFile(certificatePath) + + pemCertificate.foreach { pem => + val isValid = validateCertificate(pem) + logger.info(s"Certificate verification result: $isValid") + } + + loadTrustStore().foreach { trustStore => + logger.info(s"Trust Store contains ${trustStore.size()} certificates.") + } + } +} diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 8e118129d..8e28a23ab 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -279,6 +279,7 @@ object ErrorMessages { val X509ThereAreNoPsd2Roles = "OBP-20308: PEM Encoded Certificate does not contain PSD2 roles." val X509CannotGetPublicKey = "OBP-20309: Public key cannot be found in the PEM Encoded Certificate." val X509PublicKeyCannotVerify = "OBP-20310: Certificate's public key cannot be used to verify signed request." + val X509PublicKeyCannotBeValidated = "OBP-20312: Certificate's public key cannot be validated." val X509RequestIsNotSigned = "OBP-20311: The Request is not signed." // OpenID Connect diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 1618e33e2..cd749c3f9 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -6,7 +6,7 @@ import java.util.UUID.randomUUID import akka.http.scaladsl.model.HttpMethod import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT} import code.api.{APIFailureNewStyle, Constant, JsonResponseException} -import code.api.Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID +import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID} import code.api.cache.Caching import code.api.util.APIUtil._ import code.api.util.ApiRole.canCreateAnyTransactionRequest @@ -73,11 +73,11 @@ import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo} import code.atmattribute.AtmAttribute import code.bankattribute.BankAttribute import code.connectormethod.{ConnectorMethodProvider, JsonConnectorMethod} -import code.counterpartylimit.{CounterpartyLimit} +import code.counterpartylimit.CounterpartyLimit import com.openbankproject.commons.model.CounterpartyLimitTrait import code.crm.CrmEvent import code.crm.CrmEvent.CrmEvent -import com.openbankproject.commons.model.{CustomerAccountLinkTrait, AgentAccountLinkTrait} +import com.openbankproject.commons.model.{AgentAccountLinkTrait, CustomerAccountLinkTrait} import code.dynamicMessageDoc.{DynamicMessageDocProvider, JsonDynamicMessageDoc} import code.dynamicResourceDoc.{DynamicResourceDocProvider, JsonDynamicResourceDoc} import code.endpointMapping.{EndpointMappingProvider, EndpointMappingT} @@ -364,6 +364,20 @@ object NewStyle extends MdcLoggable{ } } + def getAccountCanReadBalancesOfBerlinGroup(user : User, callContext: Option[CallContext]): OBPReturnType[List[BankIdAccountId]] = { + val viewIds = List(ViewId(SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID)) + Views.views.vend.getPrivateBankAccountsFuture(user, viewIds) map { i => + (i, callContext ) + } + } + + def getAccountCanReadTransactionsOfBerlinGroup(user : User, callContext: Option[CallContext]): OBPReturnType[List[BankIdAccountId]] = { + val viewIds = List(ViewId(Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID)) + Views.views.vend.getPrivateBankAccountsFuture(user, viewIds) map { i => + (i, callContext ) + } + } + def getAccountListOfBerlinGroup(user : User, callContext: Option[CallContext]): OBPReturnType[List[BankIdAccountId]] = { val viewIds = List(ViewId(SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID)) Views.views.vend.getPrivateBankAccountsFuture(user, viewIds) map { i => diff --git a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala index 22e366afc..0024598c0 100644 --- a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala @@ -72,7 +72,9 @@ trait OBPDataImport extends MdcLoggable { protected def dataOrFirstFailure[T](boxes : List[Box[T]]) = { val firstFailure = boxes.collectFirst{case f: Failure => f} firstFailure match { - case Some(f) => f + case Some(f) => + logger.debug(f) + f case None => Full(boxes.flatten) //no failures, so we can return the results } } diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 9b8956ef9..59bcbfb57 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -1,89 +1,302 @@ /** -Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ + * Open Bank Project - API + * Copyright (C) 2011-2019, TESOBE GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * Email: contact@tesobe.com + * TESOBE GmbH. + * Osloer Strasse 16/17 + * Berlin 13359, Germany + * + * This product includes software developed at + * TESOBE (http://www.tesobe.com/) + */ package code.snippet +import code.accountholders.AccountHolders import code.api.RequestHeader -import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{GetConsentResponseJson, createGetConsentResponseJson} -import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil} +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson, GetConsentResponseJson, createGetConsentResponseJson} +import code.api.util.ErrorMessages.ConsentNotFound +import code.api.util._ import code.api.v3_1_0.APIMethods310 import code.api.v5_0_0.APIMethods500 import code.api.v5_1_0.APIMethods510 import code.consent.{ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers -import code.model.dataAccess.AuthUser +import code.model.dataAccess.{AuthUser, BankAccountRouting} import code.util.Helper.{MdcLoggable, ObpS} +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.model.BankIdAccountId import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.http.js.JsCmds import net.liftweb.http.rest.RestHelper -import net.liftweb.http.{RequestVar, S, SHtml, SessionVar} +import net.liftweb.http.{S, SHtml, SessionVar} import net.liftweb.json.{Formats, parse} +import net.liftweb.mapper.By import net.liftweb.util.CssSel import net.liftweb.util.Helpers._ import scala.collection.immutable +import scala.concurrent.Future +import scala.xml.NodeSeq +/** + * This class handles Berlin Group consent requests. + * It provides functionality to confirm or deny consent requests, + * and manages the consent process for accessing account data. + */ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { + // Custom JSON formats for serialization/deserialization protected implicit override def formats: Formats = CustomJsonFormats.formats - private object otpValue extends SessionVar("123") - private object redirectUriValue extends SessionVar("") + // Session variables to store OTP, redirect URI, and other consent-related data + private object otpValue extends SessionVar("123") // Stores the OTP value for SCA (Strong Customer Authentication) + private object redirectUriValue extends SessionVar("") // Stores the redirect URI for post-consent actions + private object updateConsentPayloadValue extends SessionVar(false) // Flag to indicate if consent payload needs updating + private object userIsOwnerOfAccountsValue extends SessionVar(true) // Flag to check if the user owns the accounts + // Session variables to store selected IBANs for accounts, balances, and transactions + private object selectedAccountsIbansValue extends SessionVar[Set[String]](Set()) { + override def set(value: Set[String]): Set[String] = { + logger.debug(s"selectedAccountsIbansValue changed to: ${value.mkString(", ")}") + super.set(value) + } + } + private object accessAccountsDefinedVar extends SessionVar(true) + private object accessBalancesDefinedVar extends SessionVar(true) + private object accessTransactionsDefinedVar extends SessionVar(true) + /** + * Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions. + * + * @param accounts List of IBANs for accounts. + * @param balances List of IBANs for balances. + * @param transactions List of IBANs for transactions. + * @return ConsentAccessJson object. + */ + def createConsentAccessJson(accounts: List[String], balances: List[String], transactions: List[String]): ConsentAccessJson = { + val accountsList = accounts.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + val balancesList = balances.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + val transactionsList = transactions.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + + ConsentAccessJson( + accounts = Some(accountsList), // Populate accounts + balances = if (balancesList.nonEmpty) Some(balancesList) else None, // Populate balances + transactions = if (transactionsList.nonEmpty) Some(transactionsList) else None // Populate transactions + ) + } + + /** + * Updates the consent with new IBANs for accounts, balances, and transactions. + * + * @param consentId The ID of the consent to update. + * @param ibans List of IBANs for accounts. + * @return Future[MappedConsent] representing the updated consent. + */ + private def updateConsent(consentId: String, ibans: List[String], canReadBalances: Boolean, canReadTransactions: Boolean): Future[MappedConsent] = { + for { + // Fetch the consent by ID + consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404) + } + // Update the consent JWT with new access details + consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( + createConsentAccessJson( + ibans, + if(canReadBalances) ibans else List(), + if(canReadTransactions) ibans else List() + ), + consent, + None + ) map { + i => APIUtil.connectorEmptyResponse(i, None) + } + // Save the updated consent + updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { + i => APIUtil.connectorEmptyResponse(i, None) + } + } yield { + updatedConsent + } + } + + /** + * Renders the Berlin Group consent confirmation form. + * + * @return CssSel for rendering the form. + */ def confirmBerlinGroupConsentRequest: CssSel = { callGetConsentByConsentId() match { case Full(consent) => + // Set OTP and redirect URI from the consent otpValue.set(consent.challenge) val json: GetConsentResponseJson = createGetConsentResponseJson(consent) val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consent.consumerId) val consentJwt: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(parse(_) .extract[ConsentJWT]) - val tppRedirectUri: immutable.Seq[String] = consentJwt.map{ h => + val tppRedirectUri: immutable.Seq[String] = consentJwt.map { h => h.request_headers.filter(h => h.name == RequestHeader.`TPP-Redirect-URL`) }.getOrElse(Nil).map((_.values.mkString(""))) val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com") redirectUriValue.set(uri) + + // Get all accounts held by the current user + val userAccounts: Set[BankIdAccountId] = + AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet + val userIbans: Set[String] = userAccounts.flatMap { acc => + BankAccountRouting.find( + By(BankAccountRouting.BankId, acc.bankId.value), + By(BankAccountRouting.AccountId, acc.accountId.value), + By(BankAccountRouting.AccountRoutingScheme, "IBAN") + ).map(_.AccountRoutingAddress.get) + } + // Select all IBANs + selectedAccountsIbansValue.set(userIbans) + + // Determine which IBANs the user can access for accounts, balances, and transactions + val canReadAccountsIbans: List[String] = json.access.accounts match { + case Some(accounts) if accounts.isEmpty => // Access is requested + updateConsentPayloadValue.set(true) + accessAccountsDefinedVar.set(true) + userIbans.toList + case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessAccountsDefinedVar.set(true) + accounts.flatMap(_.iban) + case Some(accounts) => // Logged in user is not an owner of IBAN/IBANs + userIsOwnerOfAccountsValue.set(false) + accessAccountsDefinedVar.set(true) + accounts.flatMap(_.iban) + case None => // Access is not requested + accessAccountsDefinedVar.set(false) + List() + } + val canReadBalancesIbans: List[String] = json.access.balances match { + case Some(balances) if balances.isEmpty => // Access is requested + updateConsentPayloadValue.set(true) + accessBalancesDefinedVar.set(true) + userIbans.toList + case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessBalancesDefinedVar.set(true) + balances.flatMap(_.iban) + case Some(balances) => // Logged in user is not an owner of IBAN/IBANs + userIsOwnerOfAccountsValue.set(false) + accessBalancesDefinedVar.set(true) + balances.flatMap(_.iban) + case None => // Access is not requested + accessBalancesDefinedVar.set(false) + List() + } + val canReadTransactionsIbans: List[String] = json.access.transactions match { + case Some(transactions) if transactions.isEmpty => // Access is requested + updateConsentPayloadValue.set(true) + accessTransactionsDefinedVar.set(true) + userIbans.toList + case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessTransactionsDefinedVar.set(true) + transactions.flatMap(_.iban) + case Some(transactions) => // Logged in user is not an owner of IBAN/IBANs + userIsOwnerOfAccountsValue.set(false) + accessTransactionsDefinedVar.set(true) + transactions.flatMap(_.iban) + case None => // Access is not requested + accessTransactionsDefinedVar.set(false) + List() + } + + /** + * Generates toggle switches for IBAN lists. + * + * @param scope The scope of the IBANs (e.g., "canReadAccountsIbans"). + * @param ibans List of IBANs to display. + * @param selectedList Set of currently selected IBANs. + * @param sessionVar Session variable to update when toggling. + * @return Sequence of NodeSeq representing the toggle switches. + */ + def generateCheckboxes(scope: String, ibans: List[String], selectedList: Set[String], sessionVar: SessionVar[Set[String]]): immutable.Seq[NodeSeq] = { + ibans.map { iban => + if (updateConsentPayloadValue.is) { + // Show toggle switch when updateConsentPayloadValue is true +
+ + + {iban} + +
+ } else { + // Show only the IBAN text when updateConsentPayloadValue is false + + {iban} + + } + } + } + + // Form text and user details + val currentUser = AuthUser.currentUser + val firstName = currentUser.map(_.firstName.get).getOrElse("") + val lastName = currentUser.map(_.lastName.get).getOrElse("") + val consumerName = consumer.map(_.name.get).getOrElse("") val formText = - s"""I, ${AuthUser.currentUser.map(_.firstName.get).getOrElse("")} ${AuthUser.currentUser.map(_.lastName.get).getOrElse("")}, consent to the service provider ${consumer.map(_.name.get).getOrElse("")} making actions on my behalf. - | - |This consent must respects the following actions: - | - | 1) Can read accounts: ${json.access.accounts.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | 2) Can read balances: ${json.access.balances.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | 3) Can read transactions: ${json.access.transactions.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | - |This consent will end on date ${json.validUntil}. - | - |I understand that I can revoke this consent at any time. - |""".stripMargin + s"""I, $firstName $lastName, consent to the service provider $consumerName making the following actions on my behalf: + |""".stripMargin + // Converting formText into a NodeSeq for raw HTML + val formTextHtml: NodeSeq = scala.xml.XML.loadString("
" + formText + "
") - "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & - "#confirm-bg-consent-request-form-text *" #> s"""$formText""" & + // Form rendering + "#confirm-bg-consent-request-form-title *" #> "Please confirm or deny the following consent request:" & + "#confirm-bg-consent-request-form-text *" #> ( +
+

+ {formTextHtml} +

+
+

Allowed actions:

+

Read account details

+

Read account balances

+

Read transactions

+
+
+

Accounts:

+
+ {generateCheckboxes("canReadAccountsIbans", userIbans.toList, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+
+
+

This consent will end on date {json.validUntil}.

+

I understand that I can revoke this consent at any time.

+
+ ) & { + if (userIsOwnerOfAccountsValue) { "#confirm-bg-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) & - "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) + "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) + } else { + S.error(s"User $firstName $lastName is not owner of listed accounts") + "#confirm-bg-consent-request-confirm-submit-button" #> "" & + "#confirm-bg-consent-request-deny-submit-button" #> "" + }} + case everythingElse => S.error(everythingElse.toString) "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & @@ -91,6 +304,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } + /** + * Fetches a consent by its ID. + * + * @return Box[MappedConsent] containing the consent if found. + */ private def callGetConsentByConsentId(): Box[MappedConsent] = { val requestParam = List( ObpS.param("CONSENT_ID"), @@ -103,12 +321,31 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } + /** + * Handles the confirmation of a consent request. + */ private def confirmConsentRequestProcess() = { - val consentId = ObpS.param("CONSENT_ID") openOr ("") - S.redirectTo( - s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" - ) + if (selectedAccountsIbansValue.is.isEmpty) { + S.error(s"Please select at least 1 account") + } else { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + if (updateConsentPayloadValue.is) { + updateConsent( + consentId, + selectedAccountsIbansValue.is.toList, + accessBalancesDefinedVar.is, + accessTransactionsDefinedVar.is + ) + } + S.redirectTo( + s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" + ) + } } + + /** + * Handles the denial of a consent request. + */ private def denyConsentRequestProcess() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected) @@ -116,6 +353,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 s"$redirectUriValue?CONSENT_ID=${consentId}" ) } + + /** + * Handles the confirmation of a consent request with SCA (Strong Customer Authentication). + */ private def confirmConsentRequestProcessSca() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.getConsentByConsentId(consentId) match { @@ -129,7 +370,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - + /** + * Renders the SCA confirmation form for Berlin Group consent. + * + * @return CssSel for rendering the form. + */ def confirmBgConsentRequest: CssSel = { "#otp-value" #> SHtml.text(otpValue, otpValue(_)) & "type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca) diff --git a/obp-api/src/main/webapp/confirm-bg-consent-request.html b/obp-api/src/main/webapp/confirm-bg-consent-request.html index d3e2ffcff..d07e0318a 100644 --- a/obp-api/src/main/webapp/confirm-bg-consent-request.html +++ b/obp-api/src/main/webapp/confirm-bg-consent-request.html @@ -32,7 +32,7 @@ Berlin 13359, Germany