Merge remote-tracking branch 'OBP-API/develop' into develop

# Conflicts:
#	.github/workflows/build_package.yml
This commit is contained in:
Hongwei 2025-03-07 10:05:14 +01:00
commit c073aed733
20 changed files with 749 additions and 366 deletions

View File

@ -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: |

View File

@ -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: |

View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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"))

View File

@ -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)

View File

@ -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",

View File

@ -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",

View File

@ -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)

View File

@ -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
}

View File

@ -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.")
}
}
}

View File

@ -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

View File

@ -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 =>

View File

@ -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
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
*
* 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
<div class="toggle-container">
<label class="switch">
{SHtml.ajaxCheckbox(selectedList.contains(iban), checked => {
if (checked) {
sessionVar.set(selectedList + iban) // Add to selected
} else {
sessionVar.set(selectedList - iban) // Remove from selected
}
JsCmds.Noop // Prevents page reload
}, "id" -> (iban + scope), "class" -> "toggle-input")}<span class="slider round"></span>
</label>
<span style="all: unset;" class="toggle-label">
{iban}
</span>
</div>
} else {
// Show only the IBAN text when updateConsentPayloadValue is false
<span style="all: unset;" class="toggle-label">
{iban}
</span>
}
}
}
// 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 <strong>$consumerName</strong> making the following actions on my behalf:
|""".stripMargin
// Converting formText into a NodeSeq for raw HTML
val formTextHtml: NodeSeq = scala.xml.XML.loadString("<div>" + formText + "</div>")
"#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 *" #> (
<div>
<p>
{formTextHtml}
</p>
<div>
<p><strong>Allowed actions:</strong></p>
<p style="padding-left: 20px">Read account details</p>
<p style={if (accessBalancesDefinedVar.is) "padding-left: 20px;" else "padding-left: 20px; display: none;"}>Read account balances</p>
<p style={if (accessTransactionsDefinedVar.is) "padding-left: 20px;" else "padding-left: 20px; display: none;"}>Read transactions</p>
</div>
<div>
<p><strong>Accounts</strong>:</p>
<div style="padding-left: 20px">
{generateCheckboxes("canReadAccountsIbans", userIbans.toList, selectedAccountsIbansValue.is, selectedAccountsIbansValue)}
</div>
<br/>
</div>
<p>This consent will end on date {json.validUntil}.</p>
<p>I understand that I can revoke this consent at any time.</p>
</div>
) & {
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)

View File

@ -32,7 +32,7 @@ Berlin 13359, Germany
<div class="form-group">
<h3 id="confirm-bg-consent-request-form-title">Please check the Berlin Group Consent Request: </h3>
<div id="confirm-bg-consent-request-form-text-div">
<pre id="confirm-bg-consent-request-form-text"></pre>
<div id="confirm-bg-consent-request-form-text"></div>
</div>
</div>
<form method="post">