diff --git a/.gitignore b/.gitignore index 2fb8bc7de..41be7fc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ tgz ## Backup Files *.bck *.bak +bin ######## Python - inspired by https://github.com/github/gitignore/blob/main/Python.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 3007f390c..9d0e1bbf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,12 +26,22 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## [Unreleased] ### Added +- SingleApiRequest class for the requested data for the single API. +- SingleApiConfig class to had configurations related to the single API. +- ContractService class to move all the processing logic from the API and Contract controllers to this service. - Added the following Industry Core changes to the policy and digital twin: - Added `manufacturerId` and `digitalTwinType` to the specificAssetIds in digital twin registry - Added localIdentifiers to the SerialPart aspect model +- Updated ApiController with the singleApi POST method. +- Updated ContractController by creating call methods (create, search, agree and status) without the authentication step to call in the Single API. +- Updated AuthenticationService by adding the isSingleApiAuthenticated method to authenticate the single API key. +- Updated application.yaml with the single api configurations. +- Updated deployment-backend.yaml with the oauth.apiKey. +- Updated values-int/beta/dev.yaml files with the oauth.apiKey. ### Updated - Refactored workflows where required @@ -43,6 +53,12 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e - Updated documentation references where required - Updated infrastructure guide - Updated testdata script to allow EDC constrained policy for the registry +- Updated ApiController with the singleApi POST method. +- Updated ContractController by creating call methods (create, search, agree and status) without the authentication step to call in the Single API. +- Updated AuthenticationService by adding the isSingleApiAuthenticated method to authenticate the single API key. +- Updated application.yaml with the single api configurations. +- Updated deployment-backend.yaml with the oauth.apiKey. +- Updated values-int/beta/dev.yaml files with the oauth.apiKey. - Refactored the swagger workflow ### Deleted diff --git a/charts/digital-product-pass/Chart.yaml b/charts/digital-product-pass/Chart.yaml index 691221661..d9ff9ebae 100644 --- a/charts/digital-product-pass/Chart.yaml +++ b/charts/digital-product-pass/Chart.yaml @@ -42,10 +42,10 @@ type: application # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.2.0 +version: 2.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "2.2.0" +appVersion: "2.3.0" diff --git a/charts/digital-product-pass/templates/configmap-backend.yaml b/charts/digital-product-pass/templates/configmap-backend.yaml index 417b03dd6..5c5e9436e 100644 --- a/charts/digital-product-pass/templates/configmap-backend.yaml +++ b/charts/digital-product-pass/templates/configmap-backend.yaml @@ -25,8 +25,8 @@ apiVersion: v1 kind: ConfigMap metadata: - name: { { .Release.Name } }-backend-config - namespace: { { .Values.namespace } } + name: {{ .Release.Name }}-backend-config + namespace: {{ .Values.namespace }} data: application.yaml: |- spring: @@ -74,6 +74,8 @@ data: startUpChecks: bpnCheck: {{ .Values.backend.securityCheck.bpn }} edcCheck: {{ .Values.backend.securityCheck.edc }} + authentication: + header: {{ .Values.oauth.apiKey.header }} # -- irs configuration irs: enabled: {{ .Values.backend.irs.enabled }} # -- Enable search for children in the requests @@ -123,6 +125,10 @@ data: # -- edc discovery configuration edc: key: {{ tpl (.Values.backend.discovery.edcDiscovery.key | default "bpn") . | quote }} + # -- configuration to the single API endpoint + singleApi: + maxRetries: {{ .Values.backend.singleApi.maxRetries }} + delay: {{ .Values.backend.singleApi.delay }} # -- process configuration process: # -- directory for storing the contract negotiation files diff --git a/charts/digital-product-pass/templates/deployment-backend.yaml b/charts/digital-product-pass/templates/deployment-backend.yaml index 626ff7059..0387474fa 100644 --- a/charts/digital-product-pass/templates/deployment-backend.yaml +++ b/charts/digital-product-pass/templates/deployment-backend.yaml @@ -58,27 +58,32 @@ spec: valueFrom: secretKeyRef: key: appId - name: {{ .Release.Name }}-avp-consumer-backend-auth + name: {{ .Release.Name }}-backend-auth - name: "client.id" valueFrom: secretKeyRef: key: clientId - name: {{ .Release.Name }}-avp-consumer-backend-auth + name: {{ .Release.Name }}-backend-auth - name: "client.secret" valueFrom: secretKeyRef: key: clientSecret - name: {{ .Release.Name }}-avp-consumer-backend-auth + name: {{ .Release.Name }}-backend-auth - name: "edc.apiKey" valueFrom: secretKeyRef: key: xApiKey - name: {{ .Release.Name }}-avp-consumer-backend-edc-oauth + name: {{ .Release.Name }}-backend-edc-oauth - name: "edc.participantId" valueFrom: secretKeyRef: key: participantId - name: {{ .Release.Name }}-avp-consumer-backend-edc-oauth + name: {{ .Release.Name }}-backend-edc-oauth + - name: "oauth.apiKey" + valueFrom: + secretKeyRef: + key: xApiKey + name: {{ .Release.Name }}-backend-auth volumeMounts: {{- toYaml .Values.backend.volumeMounts | nindent 12 }} ports: @@ -101,4 +106,3 @@ spec: {{- end }} volumes: {{- tpl (toYaml .Values.backend.volumes | nindent 12) .}} - diff --git a/charts/digital-product-pass/templates/secret-backend.yaml b/charts/digital-product-pass/templates/secret-backend.yaml index 3662096a4..b6bd71d12 100644 --- a/charts/digital-product-pass/templates/secret-backend.yaml +++ b/charts/digital-product-pass/templates/secret-backend.yaml @@ -25,7 +25,7 @@ apiVersion: v1 kind: Secret metadata: - name: {{ .Release.Name }}-avp-consumer-backend-auth + name: {{ .Release.Name }}-backend-auth labels: {{- include "chart.labels" . | nindent 4 }} namespace: {{ .Values.namespace }} @@ -34,12 +34,14 @@ stringData: appId: {{ .Values.oauth.appId }} clientId: {{ .Values.oauth.techUser.clientId }} clientSecret: {{ .Values.oauth.techUser.clientSecret }} + xApiKey: {{ .Values.oauth.apiKey.secret }} + --- apiVersion: v1 kind: Secret metadata: - name: {{ .Release.Name }}-avp-consumer-backend-edc-oauth + name: {{ .Release.Name }}-backend-edc-oauth labels: {{- include "chart.labels" . | nindent 4 }} namespace: {{ .Values.namespace }} diff --git a/charts/digital-product-pass/values-beta.yaml b/charts/digital-product-pass/values-beta.yaml index ec85bd1f0..1519c7675 100644 --- a/charts/digital-product-pass/values-beta.yaml +++ b/charts/digital-product-pass/values-beta.yaml @@ -105,4 +105,7 @@ oauth: enabled: true bpn: *bpn roleCheck: - enabled: false + enabled: false + apiKey: + header: "X-Api-Key" + secret: diff --git a/charts/digital-product-pass/values-dev.yaml b/charts/digital-product-pass/values-dev.yaml index 637f41c95..ffd4d8f5d 100644 --- a/charts/digital-product-pass/values-dev.yaml +++ b/charts/digital-product-pass/values-dev.yaml @@ -106,4 +106,7 @@ oauth: enabled: true bpn: *bpn roleCheck: - enabled: false \ No newline at end of file + enabled: false + apiKey: + header: "X-Api-Key" + secret: diff --git a/charts/digital-product-pass/values-int.yaml b/charts/digital-product-pass/values-int.yaml index 61edecc55..7e387aed7 100644 --- a/charts/digital-product-pass/values-int.yaml +++ b/charts/digital-product-pass/values-int.yaml @@ -83,6 +83,11 @@ backend: discovery: hostname: "semantics.int.demo.catena-x.net/discoveryfinder" + singleApi: + maxRetries: 30 + delay: 1000 + + frontend: ingress: enabled: true @@ -126,4 +131,8 @@ oauth: enabled: true bpn: *bpn roleCheck: - enabled: false + enabled: false + apiKey: + header: "X-Api-Key" + secret: + diff --git a/charts/digital-product-pass/values.yaml b/charts/digital-product-pass/values.yaml index 10317f71e..beb5b9428 100644 --- a/charts/digital-product-pass/values.yaml +++ b/charts/digital-product-pass/values.yaml @@ -254,6 +254,10 @@ backend: # -- edc discovery configuration edcDiscovery: key: "bpn" + # -- configuration to the single API endpoint + singleApi: + maxRetries: 30 + delay: 1000 frontend: name: "dpp-frontend" @@ -366,6 +370,11 @@ oauth: roleCheck: enabled: false + # -- to authenticate against single API + apiKey: + header: "X-Api-Key" + secret: "" + # Following Catena-X Helm Best Practices @url: https://catenax-ng.github.io/docs/kubernetes-basics/helm # @url: https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits resources: diff --git a/docs/arc42/Arc42.md b/docs/arc42/Arc42.md index eb9f63127..5bad6147e 100644 --- a/docs/arc42/Arc42.md +++ b/docs/arc42/Arc42.md @@ -390,6 +390,11 @@ This API is responsible for retrieving the Aspect Model Payloads and some metada Swagger Documentation: [https://dpp.int.demo.catena-x.net/swagger-ui/index.html](https://dpp.int.demo.catena-x.net/swagger-ui/index.html) +#### Single API +The Single API permits to get data from a Catena-X Provider by abstracting of all the separated APIs needed to do so. Authenticating with an defined API Key and with the mandatory and given serialized and discovery identifications, this API will +create the process and check for the viability of the data retrieval, searches for a passport with the given serialized id, automatically signs the contract retrieved from provider and start negotiation, waits for the negotiation +to be done and returns the data negotiated and transferred. In short, it's the set of the various APIs in one with auto-sign functionality to agile the data retrieval in a simple way. + ### Item Relationship Service Integration (Drill Down Functionality) diff --git a/dpp-backend/charts/digital-product-pass-backend/Chart.yaml b/dpp-backend/charts/digital-product-pass-backend/Chart.yaml index db503e686..0dcf2ea44 100644 --- a/dpp-backend/charts/digital-product-pass-backend/Chart.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/Chart.yaml @@ -42,10 +42,10 @@ type: application # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.2.0 +version: 2.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "2.2.0" +appVersion: "2.3.0" diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/_helpers.tpl b/dpp-backend/charts/digital-product-pass-backend/templates/_helpers.tpl index b0180a453..2735b00f6 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/_helpers.tpl +++ b/dpp-backend/charts/digital-product-pass-backend/templates/_helpers.tpl @@ -26,7 +26,7 @@ Expand the name of the chart. */}} {{- define "chart.name" -}} -{{- default .Chart.Name .Values.name | trunc 63 | trimSuffix "-" }} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* @@ -36,9 +36,9 @@ If release name contains chart name it will be used as a full name. */}} {{- define "chart.fullname" -}} {{- if .Values.fullnameOverride }} -{{- .Values.name | trunc 63 | trimSuffix "-" }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} -{{- $name := default .Chart.Name .Values.name }} +{{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} @@ -57,6 +57,7 @@ Create chart name and version as used by the chart label. {{/* Common labels */}} + {{- define "chart.labels" -}} helm.sh/chart: {{ include "chart.chart" . }} {{ include "chart.selectorLabels" . }} diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/configmap.yaml b/dpp-backend/charts/digital-product-pass-backend/templates/configmap.yaml index 68bddc77b..d2516d26a 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/configmap.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/templates/configmap.yaml @@ -25,10 +25,10 @@ apiVersion: v1 kind: ConfigMap metadata: - name: backend-config + name: {{ .Release.Name }}-backend-config namespace: {{ .Values.namespace }} data: - application.yaml: |- + application.yaml: |- spring: name: "Digital Product Passport Consumer Backend" main: @@ -57,6 +57,7 @@ data: # -- edc consumer connection configuration edc: endpoint: "https://{{ .Values.edc.hostname }}" + readiness: {{ .Values.edc.apis.readiness }} management: {{ .Values.edc.apis.management }} catalog: {{ .Values.edc.apis.catalog }} negotiation: {{ .Values.edc.apis.negotiation }} @@ -73,6 +74,8 @@ data: startUpChecks: bpnCheck: {{ .Values.securityCheck.bpn }} edcCheck: {{ .Values.securityCheck.edc }} + authentication: + header: {{ .Values.oauth.apiKey.header }} # -- irs configuration irs: enabled: {{ .Values.irs.enabled }} # -- Enable search for children in the requests @@ -123,6 +126,10 @@ data: # -- edc discovery configuration edc: key: {{ tpl (.Values.discovery.edcDiscovery.key | default "bpn") . | quote }} + # -- configuration to the single API endpoint + singleApi: + maxRetries: {{ .Values.singleApi.maxRetries }} + delay: {{ .Values.singleApi.delay }} # -- process configuration process: # -- directory for storing the contract negotiation files diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/deployment.yaml b/dpp-backend/charts/digital-product-pass-backend/templates/deployment.yaml index a447670f9..310266acf 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/deployment.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/templates/deployment.yaml @@ -65,36 +65,34 @@ spec: valueFrom: secretKeyRef: key: appId - name: avp-consumer-backend-oauth + name: {{ .Release.Name }}-backend-auth - name: "client.id" valueFrom: secretKeyRef: key: clientId - name: consumer-backend-oauth + name: {{ .Release.Name }}-backend-auth - name: "client.secret" valueFrom: secretKeyRef: key: clientSecret - name: consumer-backend-oauth + name: {{ .Release.Name }}-backend-auth - name: "edc.apiKey" valueFrom: secretKeyRef: key: xApiKey - name: consumer-edc + name: {{ .Release.Name }}-backend-edc-oauth - name: "edc.participantId" valueFrom: secretKeyRef: key: participantId - name: consumer-edc + name: {{ .Release.Name }}-backend-edc-oauth + - name: "oauth.apiKey" + valueFrom: + secretKeyRef: + key: xApiKey + name: {{ .Release.Name }}-backend-auth volumeMounts: - - name: backend-config - mountPath: /app/config - - name: pvc-backend - mountPath: /app/data/process - subPath: data/process - - name: pvc-backend - mountPath: /app/log - subPath: log + {{- toYaml .Values.volumeMounts | nindent 12 }} ports: - name: http containerPort: 8888 @@ -122,9 +120,4 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} volumes: - - name: backend-config - configMap: - name: backend-config - - name: pvc-backend - persistentVolumeClaim: - claimName: pvc-data + {{- tpl (toYaml .Values.volumes | nindent 12) .}} diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/pvc-data.yaml b/dpp-backend/charts/digital-product-pass-backend/templates/pvc-data.yaml index 97e26776f..1be674324 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/pvc-data.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/templates/pvc-data.yaml @@ -25,7 +25,7 @@ kind: PersistentVolumeClaim apiVersion: v1 metadata: - name: pvc-data + name: {{ .Release.Name }}-pvc-data labels: {{- include "chart.labels" . | nindent 4 }} namespace: {{ .Values.namespace }} diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/secret.yaml b/dpp-backend/charts/digital-product-pass-backend/templates/secret.yaml index 022c91291..e8edcdd42 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/secret.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/templates/secret.yaml @@ -25,7 +25,7 @@ apiVersion: v1 kind: Secret metadata: - name: consumer-backend-oauth + name: {{ .Release.Name }}-backend-auth labels: {{- include "chart.labels" . | nindent 4 }} namespace: {{ .Values.namespace }} @@ -34,12 +34,13 @@ stringData: appId: {{ .Values.oauth.appId }} clientId: {{ .Values.oauth.techUser.clientId }} clientSecret: {{ .Values.oauth.techUser.clientSecret }} + xApiKey: {{ .Values.oauth.apiKey.secret }} --- apiVersion: v1 kind: Secret metadata: - name: consumer-edc + name: {{ .Release.Name }}-backend-edc-oauth labels: {{- include "chart.labels" . | nindent 4 }} namespace: {{ .Values.namespace }} diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/service.yaml b/dpp-backend/charts/digital-product-pass-backend/templates/service.yaml index b21a69355..3d24466e0 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/service.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/templates/service.yaml @@ -23,26 +23,19 @@ ################################################################################# apiVersion: v1 -kind: Secret +kind: Service metadata: - name: avp-consumer-backend-cx-registry-auth - labels: - {{- include "chart.labels" . | nindent 4 }} + name: {{ .Values.name }} namespace: {{ .Values.namespace }} -type: Opaque -stringData: - clientId: {{ .Values.oauth.techUser.clientId }} - clientSecret: {{ .Values.oauth.techUser.clientSecret }} ---- - -apiVersion: v1 -kind: Secret -metadata: - name: avp-consumer-backend-edc-oauth labels: {{- include "chart.labels" . | nindent 4 }} - namespace: {{ .Values.namespace }} -type: Opaque -stringData: - xApiKey: {{ .Values.edc.xApiKey }} - participantId: {{ .Values.edc.participantId }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} # host port + targetPort: 8888 # container port + protocol: TCP + name: http + selector: + {{- include "chart.selectorLabels" . | nindent 4 }} + component: backend diff --git a/dpp-backend/charts/digital-product-pass-backend/templates/serviceaccount.yaml b/dpp-backend/charts/digital-product-pass-backend/templates/serviceaccount.yaml index f4c8beedf..cbf734797 100644 --- a/dpp-backend/charts/digital-product-pass-backend/templates/serviceaccount.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/templates/serviceaccount.yaml @@ -22,7 +22,6 @@ # SPDX-License-Identifier: Apache-2.0 ################################################################################# -{{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: @@ -33,4 +32,3 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} -{{- end }} diff --git a/dpp-backend/charts/digital-product-pass-backend/values-int.yaml b/dpp-backend/charts/digital-product-pass-backend/values-int.yaml index fd7700467..927a70e19 100644 --- a/dpp-backend/charts/digital-product-pass-backend/values-int.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/values-int.yaml @@ -43,7 +43,7 @@ ingress: edc: xApiKey: participantId: &bpn - endpoint: "materialpass.int.demo.catena-x.net/consumer" + hostname: "materialpass.int.demo.catena-x.net/consumer" hostname: *hostname securityCheck: @@ -60,6 +60,10 @@ process: discovery: hostname: "semantics.int.demo.catena-x.net/discoveryfinder" +singleApi: + maxRetries: 30 + delay: 1000 + oauth: hostname: "centralidp.int.demo.catena-x.net" techUser: @@ -69,6 +73,9 @@ oauth: appId: bpnCheck: enabled: true - bpn: "" + bpn: *bpn roleCheck: enabled: false + apiKey: + header: "X-Api-Key" + secret: diff --git a/dpp-backend/charts/digital-product-pass-backend/values.yaml b/dpp-backend/charts/digital-product-pass-backend/values.yaml index 625d95cfa..93aea3d00 100644 --- a/dpp-backend/charts/digital-product-pass-backend/values.yaml +++ b/dpp-backend/charts/digital-product-pass-backend/values.yaml @@ -29,6 +29,8 @@ name: "dpp-backend" replicaCount: 1 namespace: "" +nameOverride: "" +fullnameOverride: "" image: repository: docker.io/tractusx/digital-product-pass-backend @@ -58,8 +60,9 @@ edc: # -- BPN Number participantId: &bpn "" # -- edc consumer connection configuration - endpoint: "" + hostname: "" apis: + readiness: "/api/check/readiness" management: '/management/v2' catalog: '/catalog/request' negotiation: '/contractnegotiations' @@ -134,9 +137,9 @@ digitalTwinRegistry: # -- timeouts for the digital twin registry async negotiation timeouts: search: 50 - negotiation: 40 - transfer: 10 - digitalTwin: 20 + negotiation: 60 + transfer: 20 + digitalTwin: 40 # -- temporary storage of dDTRs for optimization temporaryStorage: enabled: true @@ -172,6 +175,10 @@ discovery: # -- edc discovery configuration edcDiscovery: key: "bpn" +# -- configuration to the single API endpoint +singleApi: + maxRetries: 30 + delay: 1000 # -- oauth configuration oauth: @@ -194,17 +201,80 @@ oauth: roleCheck: enabled: false + # -- to authenticate against single API + apiKey: + header: "X-Api-Key" + secret: "" + +# -- The [pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod) defines privilege and access control settings for a Pod within the deployment podSecurityContext: + seccompProfile: + # -- Restrict a Container's Syscalls with seccomp + type: RuntimeDefault + # -- Runs all processes within a pod with a special uid runAsUser: 1000 + # -- Processes within a pod will belong to this guid + runAsGroup: 3000 + # -- The owner for volumes and any files created within volumes will belong to this guid fsGroup: 3000 +# The [container security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container) defines privilege and access control settings for a Container within a pod securityContext: + capabilities: + # -- Specifies which capabilities to drop to reduce syscall attack surface + drop: + - ALL + # -- Specifies which capabilities to add to issue specialized syscalls + add: [] + # -- Whether the root filesystem is mounted in read-only mode readOnlyRootFilesystem: true + # -- Controls [Privilege Escalation](https://kubernetes.io/docs/concepts/security/pod-security-policy/#privilege-escalation) enabling setuid binaries changing the effective user ID allowPrivilegeEscalation: false + # -- Requires the container to run without root privileges runAsNonRoot: true + # -- The container's process will run with the specified uid runAsUser: 1000 + # -- The owner for volumes and any files created within volumes will belong to this guid runAsGroup: 3000 +# -- specifies the volume mounts for the backend deployment +volumeMounts: + # -- mounted path for the backend configuration added in the config maps + - name: backend-config + mountPath: /app/config + # -- contains the location for the process data directory + - name: pvc-backend + mountPath: /app/data/process + subPath: data/process + # -- contains the log directory uses by the backend + - name: tmpfs + mountPath: /app/log + subPath: log + # -- container tmp directory + - name: tmpfs + mountPath: /tmp + # -- contains the vault configuration for the backend + - name: tmpfs + mountPath: /app/data/VaultConfig + subPath: VaultConfig/vault.token.yml + # -- contains the temporary directory used by the backend + - name: tmpfs + mountPath: /app/tmp + +# -- volume claims for the containers +volumes: + # -- persist the backend configuration + - name: backend-config + configMap: + name: "{{ .Release.Name }}-backend-config" + # -- persist the backend data directories + - name: pvc-backend + persistentVolumeClaim: + claimName: "{{ .Release.Name }}-pvc-data" + # -- temporary file system mount + - name: tmpfs + emptyDir: {} + serviceAccount: # Specifies whether a service account should be created create: true diff --git a/dpp-backend/digitalproductpass/README.md b/dpp-backend/digitalproductpass/README.md index 6a84d4f2a..a72f32d07 100644 --- a/dpp-backend/digitalproductpass/README.md +++ b/dpp-backend/digitalproductpass/README.md @@ -109,16 +109,16 @@ The Digital Product Pass Open API specification is available at the swagger hub ## Digital Product Pass APIs The APIs below are the ones contain in the `Digital Product Pass Backend` reference implementation. Which can be reused for retrieving aspects from the Catena-X Network. - | API | Method | Description | Parameters | - | ------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | **/api/contract/create** | POST | The `/api/contract/create` api is responsible for calling the `BPN Discovery` service searching for the BPN of a `manufacturerPartId` and validating if there is any `Decentral Digital Twin Registry` available for the BPN number found in the `EDC Discovery` service. | [Go to Params](#apicontractcreate) - | **/api/contract/search** | POST | At the **/api/contract/search** API the user can search for a serialized Id and get its contract. The `Backend` will search for the Digital Twin and will return the contract for the first one that is found. A `sign token` (a sha256 hash) is return also and acts like a "session token" allowing just the user that created the process to sign or decline the contract. |[Go to Params](#apicontractsearch) | - | **/api/contract/agree** | POST | Once the user has the contract he can call the `/api/contract/agree` API to start the negotiation process and the transfer of the passport. This means that the user accepted the policy and the frame-contracts contained in the contract policy. | [Go to Params](#apicontractagree) | - | **/api/contract/decline** | POST | The other option rather than `/agree` is the `/decline` API, that basically blocks the process and makes it invalid. This means that the user declined the specific contract that was found for this process. | [Go to Params](#apicontractdecline) - | **/api/contract/cancel** | POST | The user can use `/cancel` to interrupt the negotiation process once it is signed by mistake if is the case. It will be only valid until the negotiation is made. | [Go to Params](#apicontractcancel) - | **/api/contract/status/``** | GET | After the user signs the contract he can use the `/status` API to get the process status and see when it is ready to retrieve the passport using the API `/data`.. | [Go to Params](#apicontractstatusprocessid) - | **/api/data** | POST | The API `/data` will decrypt the passport file that is encrypted using the session token "sign token", and will delete the file so that it is returned just once to the user and can not be accessed anymore. So a new passport will be always need to be requested.. | [Go to Params](#apidata) | - + | API | Method | Description | Parameters | + |----------------------------------------| ------ |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------| + | **/api/contract/create** | POST | The `/api/contract/create` api is responsible for calling the `BPN Discovery` service searching for the BPN of a `manufacturerPartId` and validating if there is any `Decentral Digital Twin Registry` available for the BPN number found in the `EDC Discovery` service. | [Go to Params](#apicontractcreate) + | **/api/contract/search** | POST | At the **/api/contract/search** API the user can search for a serialized Id and get its contract. The `Backend` will search for the Digital Twin and will return the contract for the first one that is found. A `sign token` (a sha256 hash) is return also and acts like a "session token" allowing just the user that created the process to sign or decline the contract. | [Go to Params](#apicontractsearch) | + | **/api/contract/agree** | POST | Once the user has the contract he can call the `/api/contract/agree` API to start the negotiation process and the transfer of the passport. This means that the user accepted the policy and the frame-contracts contained in the contract policy. | [Go to Params](#apicontractagree) | + | **/api/contract/decline** | POST | The other option rather than `/agree` is the `/decline` API, that basically blocks the process and makes it invalid. This means that the user declined the specific contract that was found for this process. | [Go to Params](#apicontractdecline) + | **/api/contract/cancel** | POST | The user can use `/cancel` to interrupt the negotiation process once it is signed by mistake if is the case. It will be only valid until the negotiation is made. | [Go to Params](#apicontractcancel) + | **/api/contract/status/``** | GET | After the user signs the contract he can use the `/status` API to get the process status and see when it is ready to retrieve the passport using the API `/data`.. | [Go to Params](#apicontractstatusprocessid) + | **/api/data** | POST | The API `/data` will decrypt the passport file that is encrypted using the session token "sign token", and will delete the file so that it is returned just once to the user and can not be accessed anymore. So a new passport will be always need to be requested.. | [Go to Params](#apidata) | + | **/api/data/request** | POST | The Single API `/data/request` calls the necessary above APIs in order to retrieve the passport with auto-sign capability, it calls the create API, then search API, signs with the agree API and retrieves the data with the data API. The authentication is done with an API Key received as an HTTP header. | [Go to Params](#apidataRequest) | ### Parameters @@ -178,6 +178,17 @@ The APIs below are the ones contain in the `Digital Product Pass Backend` refere | processId | processIdentification | [REQUIRED] | | contractId | contractIdentification | [REQUIRED] | | token | searchSessionToken | [REQUIRED] | + +#### /api/data/request + +| Parameter | Value Name | Mandatory or Optional Value | +|-----------------|------------------------|-----------------------------| +| id | searchIdValue | [REQUIRED] | +| idType | searchIdTypeName | manufacturerPartId | +| discoveryId | serializedIdValue | [REQUIRED] | +| discoveryIdType | serializedIdTypeName | partInstanceId | +| children | searchForChildren | true/false | +| semanticId | semanticIdentification | semanticId | # Detailed API Services @@ -200,6 +211,26 @@ Get data from a Catena-X Provider by using its processId, contractId and a token } ``` +### /api/data/request +The Single API permits to get data from a Catena-X Provider by abstracting of all the needed APIs to do so. Authenticating with an API Key and with the given id and discoveryId, this API will +create the process and check for the viability of the data retrieval, searches for a passport with the given id, automatically signs the contract retrieved from provider and start negotiation, waits for the negotiation +to be done and returns the data negotiated and transferred. + +```bash +/api/data/request #Creates the process, searchs for a passport with the following id, negotiates and signes the contract, returns the data negotiated and transferred. +``` +###### Request body +```json +{ + "id": "string", + "idType": "optional:string", + "discoveryId": "string", + "discoveryIdType": "optional:string", + "children": "optional:boolean", + "semanticId": "optional:string" +} +``` + ### Contract API #### /api/contract/create diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/PassportConfig.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/PassportConfig.java index db16044ac..9877e984f 100644 --- a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/PassportConfig.java +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/PassportConfig.java @@ -37,6 +37,8 @@ @Configuration @ConfigurationProperties(prefix="configuration.passport") public class PassportConfig { + + private String defaultIdType; private String searchIdSchema; private List aspects; @@ -60,6 +62,12 @@ public PassportConfig(String searchIdSchema, List aspects, PolicyCheckCo this.aspects = aspects; this.policyCheck = policyCheck; } + public PassportConfig(String searchIdSchema, List aspects, String defaultIdType, PolicyCheckConfig policyCheck) { + this.searchIdSchema = searchIdSchema; + this.aspects = aspects; + this.policyCheck = policyCheck; + this.defaultIdType = defaultIdType; + } public List getAspects() { return aspects; @@ -77,6 +85,12 @@ public void setSearchIdSchema(String searchIdSchema) { this.searchIdSchema = searchIdSchema; } + public String getDefaultIdType() { + return defaultIdType; + } + + public void setDefaultIdType(String defaultIdType) { + this.defaultIdType = defaultIdType; public PolicyCheckConfig getPolicyCheck() { return policyCheck; } diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SecurityConfig.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SecurityConfig.java index cb1b446b3..47a1ba518 100644 --- a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SecurityConfig.java +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SecurityConfig.java @@ -40,12 +40,13 @@ public class SecurityConfig { /** ATTRIBUTES **/ private AuthorizationConfig authorization; - private StartUpCheckConfig startUpChecks; + private AuthenticationConfig authentication; - public SecurityConfig(AuthorizationConfig authorization, StartUpCheckConfig startUpChecks) { + public SecurityConfig(AuthorizationConfig authorization, StartUpCheckConfig startUpChecks, AuthenticationConfig authentication) { this.authorization = authorization; this.startUpChecks = startUpChecks; + this.authentication = authentication; } public SecurityConfig() { @@ -67,6 +68,15 @@ public StartUpCheckConfig getStartUpChecks() { public void setStartUpChecks(StartUpCheckConfig startUpChecks) { this.startUpChecks = startUpChecks; } + + public AuthenticationConfig getAuthentication() { + return authentication; + } + + public void setAuthentication(AuthenticationConfig authentication) { + this.authentication = authentication; + } + /** INNER CLASSES **/ /** @@ -136,4 +146,24 @@ public void setEdcCheck(Boolean edcCheck) { } } + public static class AuthenticationConfig { + private String header; + + + public AuthenticationConfig() { + } + + public AuthenticationConfig(String header) { + this.header = header; + } + + public String getHeader() { + return header; + } + + public void setHeader(String header) { + this.header = header; + } + } + } diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SingleApiConfig.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SingleApiConfig.java new file mode 100644 index 000000000..6774e9868 --- /dev/null +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/config/SingleApiConfig.java @@ -0,0 +1,69 @@ +/********************************************************************************* + * + * Tractus-X - Digital Product Passport Application + * + * Copyright (c) 2022, 2024 BMW AG, Henkel AG & Co. KGaA + * Copyright (c) 2023, 2024 CGI Deutschland B.V. & Co. KG + * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the + * License for the specific language govern in permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.digitalproductpass.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * This class consists exclusively to define the attributes and methods needed for the Simple API configuration. + **/ +@Configuration +@ConfigurationProperties(prefix="configuration.singleApi") +public class SingleApiConfig { + + /** ATTRIBUTES **/ + private Integer maxRetries; + + private Integer delay; + + public SingleApiConfig(Integer maxRetries, Integer delay) { + this.maxRetries = maxRetries; + this.delay = delay; + } + + public SingleApiConfig() { + } + + /** GETTERS AND SETTERS **/ + public Integer getMaxRetries() { + return maxRetries; + } + + + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + } + + public Integer getDelay() { + return delay; + } + + public void setDelay(Integer delay) { + this.delay = delay; + } +} diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ApiController.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ApiController.java index 39d4b842b..811f80c71 100644 --- a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ApiController.java +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ApiController.java @@ -26,7 +26,6 @@ package org.eclipse.tractusx.digitalproductpass.http.controllers.api; -import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -38,8 +37,12 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.eclipse.tractusx.digitalproductpass.config.PassportConfig; +import org.eclipse.tractusx.digitalproductpass.config.SingleApiConfig; import org.eclipse.tractusx.digitalproductpass.managers.ProcessManager; import org.eclipse.tractusx.digitalproductpass.models.http.Response; +import org.eclipse.tractusx.digitalproductpass.models.http.requests.DiscoverySearch; +import org.eclipse.tractusx.digitalproductpass.models.http.requests.Search; +import org.eclipse.tractusx.digitalproductpass.models.http.requests.SingleApiRequest; import org.eclipse.tractusx.digitalproductpass.models.http.requests.TokenRequest; import org.eclipse.tractusx.digitalproductpass.models.manager.Process; import org.eclipse.tractusx.digitalproductpass.models.manager.Status; @@ -47,6 +50,7 @@ import org.eclipse.tractusx.digitalproductpass.models.passports.PassportResponse; import org.eclipse.tractusx.digitalproductpass.services.AasService; import org.eclipse.tractusx.digitalproductpass.services.AuthenticationService; +import org.eclipse.tractusx.digitalproductpass.services.ContractService; import org.eclipse.tractusx.digitalproductpass.services.DataTransferService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; @@ -56,10 +60,15 @@ import org.springframework.web.bind.annotation.RestController; import utils.HttpUtil; import utils.JsonUtil; +import utils.LogUtil; +import utils.ThreadUtil; +import utils.exceptions.UtilException; import java.util.List; import java.util.Map; +import static java.lang.Thread.sleep; + /** * This class consists exclusively to define the HTTP methods needed for the API controller. @@ -75,15 +84,16 @@ public class ApiController { private @Autowired HttpServletResponse httpResponse; private @Autowired DataTransferService dataService; private @Autowired AasService aasService; + private @Autowired ContractService contractService; private @Autowired Environment env; private @Autowired AuthenticationService authService; private @Autowired PassportConfig passportConfig; private @Autowired HttpUtil httpUtil; private @Autowired JsonUtil jsonUtil; private @Autowired ProcessManager processManager; + private @Autowired SingleApiConfig singleApiConfig; /** METHODS **/ - @RequestMapping(value = "/api/*", method = RequestMethod.GET) @Hidden // hide this endpoint from api documentation - swagger-ui @@ -117,92 +127,135 @@ public Response getData(@Valid @RequestBody TokenRequest tokenRequestBody) { return httpUtil.buildResponse(response, httpResponse); } try { - // Check for the mandatory fields - List mandatoryParams = List.of("processId", "contractId", "token"); - if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { + return contractService.getDataCall(httpRequest, httpResponse, tokenRequestBody); + } catch (Exception e) { + response.message = e.getMessage(); + return httpUtil.buildResponse(response, httpResponse); + } + } + + /** + * HTTP POST method to retrieve the Passport with an API Key authentication. + *

+ * @param singleApiRequestBody + * the {@code SingleApiRequestBody} object with the needed and optional parameters to retrieve the data. + * + * @return this {@code Response} HTTP response with status. + * + */ + @RequestMapping(value = "/data/request", method = {RequestMethod.POST}) + @Operation(summary = "Returns the data by abstracting from all the different API's needed to get it", responses = { + @ApiResponse(description = "Default Response Structure", content = @Content(mediaType = "application/json", + schema = @Schema(implementation = Response.class))), + @ApiResponse(description = "Content of Data Field in Response", responseCode = "200", content = @Content(mediaType = "application/json", + schema = @Schema(implementation = PassportResponse.class))), + }) + public Response singleApi(@Valid @RequestBody SingleApiRequest singleApiRequestBody) { + Response response = httpUtil.getInternalError(); + if (!authService.isApiKeyAuthenticated(httpRequest)) { + response = httpUtil.getNotAuthorizedResponse(); + return httpUtil.buildResponse(response, httpResponse); + } + try { + List mandatoryParams = List.of("id", "discoveryId"); + if (!jsonUtil.checkJsonKeys(singleApiRequestBody, mandatoryParams, ".", false)) { response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); return httpUtil.buildResponse(response, httpResponse); } - // Check for processId - String processId = tokenRequestBody.getProcessId(); - if (!processManager.checkProcess(httpRequest, processId)) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); + DiscoverySearch discoverySearch = new DiscoverySearch(); + discoverySearch.setId(singleApiRequestBody.getDiscoveryId()); + discoverySearch.setType(singleApiRequestBody.getDiscoveryIdType()); + //Call Create function + Response createResponse = contractService.createCall(httpResponse, discoverySearch); + //The Status from the response must 200 to proceed + if (createResponse.getStatus() != 200) { + return createResponse; } - - - Process process = processManager.getProcess(httpRequest, processId); - if (process == null) { - response = httpUtil.getBadRequest("The process id does not exists!"); + Map createResponseData; + try { + createResponseData = (Map) jsonUtil.toMap(createResponse.getData()); + } catch (UtilException e) { + response = httpUtil.getBadRequest("Create Call:\n" + e.getMessage()); return httpUtil.buildResponse(response, httpResponse); } - - // Get status to check for contract id - String contractId = tokenRequestBody.getContractId(); - Status status = processManager.getStatus(processId); - - if (status.historyExists("contract-decline")) { - response = httpUtil.getForbiddenResponse("The contract for this passport has been declined!"); - return httpUtil.buildResponse(response, httpResponse); + String processId= createResponseData.get("processId"); + LogUtil.printMessage("[SINGLE API] [PROCESS "+processId + "] Digital Twin Registry Found! Process Created!"); + //Call Search function + Search searchBody = new Search(); + searchBody.setId(singleApiRequestBody.getId()); + searchBody.setIdType(singleApiRequestBody.getIdType()); + searchBody.setProcessId(createResponseData.get("processId")); + searchBody.setChildren(singleApiRequestBody.getChildren()); + searchBody.setSemanticId(singleApiRequestBody.getSemanticId()); + Response searchResponse = contractService.searchCall(httpRequest, httpResponse, searchBody); + //The Status from the response must 200 to proceed + if (searchResponse.getStatus() != 200) { + return searchResponse; } - if (status.historyExists("negotiation-canceled")) { - response = httpUtil.getForbiddenResponse("This negotiation has been canceled! Please request a new one"); + Map searchResponseData; + Map contracts; + try { + searchResponseData = (Map) jsonUtil.toMap(searchResponse.getData()); + contracts = (Map) jsonUtil.toMap(searchResponseData.get("contracts")); + } catch (UtilException e) { + response = httpUtil.getBadRequest("Search Call:\n" + e.getMessage()); return httpUtil.buildResponse(response, httpResponse); } - - // Check if the contract id is correct - Map availableContracts = processManager.loadDatasets(processId); - String seedId = String.join("|",availableContracts.keySet()); // Generate Seed - // Check the validity of the token - String expectedToken = processManager.generateToken(process, seedId); - String token = tokenRequestBody.getToken(); - if (!expectedToken.equals(token)) { - response = httpUtil.getForbiddenResponse("The token is invalid!"); - return httpUtil.buildResponse(response, httpResponse); + LogUtil.printMessage("[SINGLE API] [PROCESS "+processId + "] Search for contracts done! Digital Twin and Contracts available!"); + //Call sign function + TokenRequest tokenRequest = new TokenRequest(); + tokenRequest.setToken(searchResponseData.get("token").toString()); + tokenRequest.setProcessId(searchResponseData.get("id").toString()); + String contractId = contracts.entrySet().stream().findFirst().get().getKey(); + tokenRequest.setContractId(contractId); + Response agreeResponse = contractService.doContractAgreement(httpRequest, httpResponse, tokenRequest); + LogUtil.printMessage("[SINGLE API] [PROCESS "+processId + "] Agreed with a contract and started the contract negotiation!"); + //The Status from the response must 200 to proceed + if (agreeResponse.getStatus() != 200) { + return agreeResponse; } - Dataset dataset = availableContracts.get(contractId); - - if (dataset == null) { - response.message = "The Contract Selected was not found!"; + //Call Check status function + Status status; + try { + status = (Status) jsonUtil.bindObject(agreeResponse.getData(), Status.class); + } catch (UtilException e) { + response = httpUtil.getBadRequest("Agree Call:\n" + e.getMessage()); return httpUtil.buildResponse(response, httpResponse); } - - if (!status.historyExists("data-received")) { - status = processManager.getStatus(processId); // Retry to get the status before giving an error - if(!status.historyExists("data-received")) { - response = httpUtil.getNotFound("The data is not available!"); + int retry = 1; + int maxRetries = singleApiConfig.getMaxRetries(); + while(retry <= maxRetries) { + if (status.historyExists("transfer-completed") || status.historyExists("data-received")) { + break; + } + Response statusResponse = contractService.statusCall(httpResponse, searchBody.getProcessId()); + //The Status from the response must 200 to proceed + if (statusResponse.getStatus() != 200) { + return statusResponse; + } + try{ + status = (Status) jsonUtil.bindObject(statusResponse.getData(), Status.class); + ++retry; + ThreadUtil.sleep(singleApiConfig.getDelay()); + } catch (Exception e) { + response = httpUtil.getBadRequest("Status Call:\n" + e.getMessage()); return httpUtil.buildResponse(response, httpResponse); } } - - if (status.historyExists("data-retrieved")) { - response = httpUtil.getNotFound("The data was already retrieved and is no longer available!"); - return httpUtil.buildResponse(response, httpResponse); - } - String semanticId = status.getSemanticId(); - JsonNode passport = processManager.loadPassport(processId); - if (passport == null) { - response = httpUtil.getNotFound("Failed to load passport!"); + if (retry > maxRetries) { + response = httpUtil.getBadRequest("It wasn't possible to retrieve the Passport due to exceeded number of tries!"); return httpUtil.buildResponse(response, httpResponse); } - Map negotiation = processManager.loadNegotiation(processId); - Map transfer = processManager.loadTransfer(processId); - response = httpUtil.getResponse(); - response.data = Map.of( - "metadata", Map.of( - "contract", dataset, - "negotiation", negotiation, - "transfer", transfer - ), - "aspect", passport, - "semanticId", semanticId - ); - return httpUtil.buildResponse(response, httpResponse); + LogUtil.printMessage("[SINGLE API] [PROCESS "+processId + "] Transfer process completed! Retrieving the Passport Data!"); + //Call getData function + response = contractService.getDataCall(httpRequest, httpResponse, tokenRequest); + + return response; } catch (Exception e) { response.message = e.getMessage(); return httpUtil.buildResponse(response, httpResponse); } - } + } } diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ContractController.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ContractController.java index 83730ad2c..a0246c0fe 100644 --- a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ContractController.java +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/http/controllers/api/ContractController.java @@ -26,7 +26,6 @@ package org.eclipse.tractusx.digitalproductpass.http.controllers.api; -import com.fasterxml.jackson.core.type.TypeReference; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -40,8 +39,6 @@ import org.eclipse.tractusx.digitalproductpass.config.DtrConfig; import org.eclipse.tractusx.digitalproductpass.config.PassportConfig; import org.eclipse.tractusx.digitalproductpass.config.ProcessConfig; -import org.eclipse.tractusx.digitalproductpass.exceptions.ControllerException; -import org.eclipse.tractusx.digitalproductpass.exceptions.ServiceException; import org.eclipse.tractusx.digitalproductpass.managers.DtrSearchManager; import org.eclipse.tractusx.digitalproductpass.managers.ProcessManager; import org.eclipse.tractusx.digitalproductpass.models.catenax.BpnDiscovery; @@ -55,9 +52,8 @@ import org.eclipse.tractusx.digitalproductpass.models.manager.Process; import org.eclipse.tractusx.digitalproductpass.models.manager.SearchStatus; import org.eclipse.tractusx.digitalproductpass.models.manager.Status; -import org.eclipse.tractusx.digitalproductpass.models.negotiation.catalog.Catalog; -import org.eclipse.tractusx.digitalproductpass.models.negotiation.catalog.Dataset; -import org.eclipse.tractusx.digitalproductpass.models.negotiation.policy.Set; +import org.eclipse.tractusx.digitalproductpass.models.negotiation.Catalog; +import org.eclipse.tractusx.digitalproductpass.models.negotiation.Dataset; import org.eclipse.tractusx.digitalproductpass.services.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; @@ -82,6 +78,7 @@ public class ContractController { private @Autowired HttpServletRequest httpRequest; private @Autowired HttpServletResponse httpResponse; private @Autowired DataTransferService dataService; + private @Autowired ContractService contractService; private @Autowired VaultService vaultService; private @Autowired AasService aasService; private @Autowired AuthenticationService authService; @@ -125,106 +122,7 @@ public Response create(@Valid @RequestBody DiscoverySearch searchBody) { return httpUtil.buildResponse(response, httpResponse); } - - searchBody.setType(this.discoveryConfig.getBpn().getKey()); // Set default configuration key as default - List mandatoryParams = List.of("id"); - if (!jsonUtil.checkJsonKeys(searchBody, mandatoryParams, ".", false)) { - response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); - return httpUtil.buildResponse(response, httpResponse); - } - - List bpnDiscoveries = null; - try { - bpnDiscoveries = catenaXService.getBpnDiscovery(searchBody.getId(), searchBody.getType()); - } catch (Exception e) { - response.message = "Failed to get the bpn number from the discovery service"; - response.status = 404; - response.statusText = "Not Found"; - return httpUtil.buildResponse(response, httpResponse); - } - if (bpnDiscoveries == null || bpnDiscoveries.size() == 0) { - response.message = "Failed to get the bpn number from the discovery service, discovery response is null"; - response.status = 404; - response.statusText = "Not Found"; - return httpUtil.buildResponse(response, httpResponse); - } - List bpnList = new ArrayList<>(); - for(BpnDiscovery bpnDiscovery : bpnDiscoveries){ - bpnList.addAll(bpnDiscovery.mapBpnNumbers()); - } - if(bpnList.size() == 0){ - response.message = "The asset was not found in the BPN Discovery!"; - response.status = 404; - response.statusText = "Not Found"; - return httpUtil.buildResponse(response, httpResponse); - } - String processId = processManager.initProcess(); - LogUtil.printMessage("Creating process [" + processId + "] for "+searchBody.getType() + ": "+ searchBody.getId()); - ConcurrentHashMap> dataModel = null; - if(dtrConfig.getTemporaryStorage().getEnabled()) { - try { - dataModel = this.dtrSearchManager.loadDataModel(); - } catch (Exception e) { - LogUtil.printWarning("Failed to load data model from disk!"); - } - } - // This checks if the cache is deactivated or if the bns are not in the dataModel, if one of them is not in the data model then we need to check for them - if(!dtrConfig.getTemporaryStorage().getEnabled() || ((dataModel==null) || !jsonUtil.checkJsonKeys(dataModel, bpnList, ".", false))){ - catenaXService.searchDTRs(bpnList, processId); - }else{ - boolean requestDtrs = false; - // Take the results from cache - for(String bpn: bpnList){ - List dtrs = null; - try { - dtrs = (List) jsonUtil.bindReferenceType(dataModel.get(bpn), new TypeReference>() {}); - } catch (Exception e) { - throw new ControllerException(this.getClass().getName(), e, "Could not bind the reference type!"); - } - - if(dtrs == null || dtrs.size() == 0){ - continue; - } - Long currentTimestamp = DateTimeUtil.getTimestamp(); - // Iterate over every DTR and add it to the file - for(Dtr dtr: dtrs){ - - Long validUntil = dtr.getValidUntil(); - //Check if invalid time has come - if(dtr.getInvalid() && validUntil > currentTimestamp){ - continue; - } - - if(dtr.getContractId() == null || dtr.getContractId().isEmpty() || validUntil == null || validUntil < currentTimestamp){ - requestDtrs = true; // If the cache invalidation time has come request Dtrs - break; - } - - processManager.addSearchStatusDtr(processId, dtr); //Add valid DTR to status - } - - // If the refresh from the cache is necessary - if(requestDtrs){ - dtrSearchManager.deleteBpns(dataModel, bpnList); // Delete BPN numbers - catenaXService.searchDTRs(bpnList, processId); // Start again the search - break; - } - } - } - - SearchStatus status = processManager.getSearchStatus(processId); - if(status == null){ - return httpUtil.buildResponse(httpUtil.getNotFound("It was not possible to search for the decentral digital twin registries"), httpResponse); - } - if(status.getDtrs().isEmpty()){ - return httpUtil.buildResponse(httpUtil.getNotFound("No decentral digital twin registry was found"), httpResponse); - } - response = httpUtil.getResponse(); - response.data = Map.of( - "processId", processId - ); - return httpUtil.buildResponse(response, httpResponse); - + return contractService.createCall(httpResponse, searchBody); } catch (Exception e) { assert response != null; response.message = "It was not possible to create the process and search for the decentral digital twin registries"; @@ -256,161 +154,7 @@ public Response search(@Valid @RequestBody Search searchBody) { return httpUtil.buildResponse(response, httpResponse); } try { - List mandatoryParams = List.of("id"); - if (!jsonUtil.checkJsonKeys(searchBody, mandatoryParams, ".", false)) { - response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); - return httpUtil.buildResponse(response, httpResponse); - } - - Process process = null; - AssetSearch assetSearch = null; - - // Check for processId - if(searchBody.getProcessId() == null){ - response = httpUtil.getBadRequest("No processId was found on the request body!"); - return httpUtil.buildResponse(response, httpResponse); - } - - String processId = searchBody.getProcessId(); - if(processId.isEmpty()){ - response = httpUtil.getBadRequest("Process id is required for decentral digital twin registry searches!"); - return httpUtil.buildResponse(response, httpResponse); - } - SearchStatus searchStatus = processManager.getSearchStatus(processId); - if (searchStatus == null) { - response = httpUtil.getBadRequest("The searchStatus id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - if(searchStatus.getDtrs().keySet().size() == 0){ - response = httpUtil.getBadRequest("No digital twins are available for this process!"); - return httpUtil.buildResponse(response, httpResponse); - } - Boolean childrenCondition = searchBody.getChildren(); - String logPrint = "[PROCESS " + processId + "] Creating search for "+searchBody.getIdType() + ": "+ searchBody.getId(); - if(childrenCondition != null){ - LogUtil.printMessage(logPrint + " with drilldown enabled"); - process = processManager.createProcess(processId, childrenCondition, httpRequest); // Store the children condition - }else { - LogUtil.printMessage(logPrint + " with drilldown disabled"); - process = processManager.createProcess(processId, httpRequest); - } - Status status = processManager.getStatus(processId); - if (status == null) { - response = httpUtil.getBadRequest("The status is not available!"); - return httpUtil.buildResponse(response, httpResponse); - } - assetSearch = aasService.decentralDtrSearch(process.id, searchBody); - - - if(assetSearch == null){ - LogUtil.printWarning("[PROCESS " + processId + "] The decentralized digital twin registry asset search return a null response!"); - status = processManager.getStatus(processId); - // Here start the algorithm to refresh the dtrs in the cache if the transfer was incompleted - List dtrList = new ArrayList(); - Map dtrs = searchStatus.getDtrs(); - List bpnList = new ArrayList(); - for(String dtrId: searchStatus.getDtrs().keySet()){ - // Check if any dtr search was incomplete - if(!status.historyExists("dtr-"+dtrId+"-transfer-incomplete")) { - continue; - } - // Add the dtr bpn to the update cache list - Dtr dtr = dtrs.get(dtrId); - String bpn = dtr.getBpn(); - if(!bpnList.contains(bpn)) { - bpnList.add(dtr.getBpn()); // Add bpn to delete in the cache - } - dtrList.add(dtr); - } - - // If no bpn numbers need to be updated is because there is no digital twin found - if(bpnList.size() == 0){ - response = httpUtil.getBadRequest("No digital twin was found!"); - return httpUtil.buildResponse(response, httpResponse); - } - - LogUtil.printWarning("["+dtrList.size()+"] Digital Twin Registries Contracts are invalid and need to be refreshed! For the following BPN Number(s): "+ bpnList.toString()); - // Refresh cache or search id - if(dtrConfig.getTemporaryStorage().getEnabled()) { - ConcurrentHashMap> dataModel = null; - try { - dataModel = this.dtrSearchManager.loadDataModel(); - } catch (Exception e) { - LogUtil.printWarning("Failed to load data model from disk!"); - } - dtrSearchManager.deleteBpns(dataModel, bpnList); // Delete BPN numbers - } - LogUtil.printMessage("Refreshing ["+bpnList.size()+"] BPN Number Endpoints..."); - catenaXService.searchDTRs(bpnList, processId); // Start again the search for refreshing the dtrs - assetSearch = aasService.decentralDtrSearch(process.id, searchBody); // Start again the search - if(assetSearch == null) { // If again was not found then we give an error - response = httpUtil.getBadRequest("No digital twin was found! Even after retrying the digital twin transfer!"); - return httpUtil.buildResponse(response, httpResponse); - } - } - // Assing the variables with the content - String assetId = assetSearch.getAssetId(); - String connectorAddress = assetSearch.getConnectorAddress(); - - /*[1]=========================================*/ - // Get catalog with all the contract offers - if(connectorAddress == null){ - LogUtil.printError("The connector address is empty!"); - } - if(assetId == null){ - LogUtil.printError("The assetId is empty!"); - } - Catalog catalog = null; - Map datasets = null; - Long startedTime = DateTimeUtil.getTimestamp(); - try { - catalog = dataService.getContractOfferCatalog(connectorAddress, assetId); - datasets = edcUtil.filterValidContracts(dataService.getContractOffers(catalog), this.passportConfig.getPolicyCheck()); - } catch (ServiceException e) { - LogUtil.printError("The EDC is not reachable, it was not possible to retrieve catalog! Trying again..."); - catalog = dataService.getContractOfferCatalog(connectorAddress, assetId); - datasets = edcUtil.filterValidContracts(dataService.getContractOffers(catalog), this.passportConfig.getPolicyCheck()); - if (datasets == null) { // If the contract catalog is not reachable retry... - response.message = "The EDC is not reachable, it was not possible to retrieve catalog! Please try again!"; - response.status = 502; - response.statusText = "Service Not Available"; - return httpUtil.buildResponse(response, httpResponse); - } - } - // Check if contract offer was not received - if (datasets == null) { - // Retry again... - LogUtil.printWarning("[PROCESS " + process.id + "] No asset id found for the dataset contract offers in the catalog! Requesting catalog again..."); - catalog = dataService.getContractOfferCatalog(connectorAddress, assetId); - datasets = edcUtil.filterValidContracts(dataService.getContractOffers(catalog), this.passportConfig.getPolicyCheck()); - if (datasets == null) { // If the contract catalog is not reachable retry... - response.message = "Asset Id not found in any contract!"; - response.status = 404; - response.statusText = "Not Found"; - return httpUtil.buildResponse(response, httpResponse); - } - } - processManager.setProviderBpn(processId, catalog.getParticipantId()); - String seedId = String.join("|", datasets.keySet()); - LogUtil.printDebug("[PROCESS " + process.id + "] ["+datasets.size()+"] Contracts found for asset [" + assetId + "] in EDC Endpoint [" + connectorAddress + "]"); - - response = null; - response = httpUtil.getResponse(); - response.data = Map.of( - "id", process.id, - "contracts", datasets, - "token", processManager.generateToken(process, seedId) - ); - - processManager.saveDatasets(process.id, datasets, seedId, startedTime, false); - // After the search is performed the search dir is deleted - if(!processManager.deleteSearchDir(process.id)){ - LogUtil.printError("It was not possible to delete the search.json file for the process"); - }else{ - LogUtil.printDebug("[PROCESS " + process.id + "] The tmp search directory was deleted successfully!"); - } - - return httpUtil.buildResponse(response, httpResponse); + return contractService.searchCall(httpRequest, httpResponse, searchBody); } catch (Exception e) { assert response != null; response.message = "It was not possible to search for the serialized id"; @@ -443,12 +187,7 @@ public Response status(@PathVariable String processId) { response = httpUtil.getBadRequest("The process id does not exists!"); return httpUtil.buildResponse(response, httpResponse); } - - // Get status - response = httpUtil.getResponse(); - response.data = processManager.getStatus(processId); - return httpUtil.buildResponse(response, httpResponse); - + return contractService.statusCall(httpResponse, processId); } catch (Exception e) { response.message = e.getMessage(); return httpUtil.buildResponse(response, httpResponse); @@ -475,82 +214,7 @@ public Response cancel(@Valid @RequestBody TokenRequest tokenRequestBody) { return httpUtil.buildResponse(response, httpResponse); } try { - // Check for the mandatory fields - List mandatoryParams = List.of("processId", "contractId", "token"); - if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { - response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check for processId - String processId = tokenRequestBody.getProcessId(); - if (!processManager.checkProcess(httpRequest, processId)) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - - Process process = processManager.getProcess(httpRequest, processId); - if (process == null) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Get status to check for contract id - String contractId = tokenRequestBody.getContractId(); - Status status = processManager.getStatus(processId); - - // Check if was already declined - if (status.historyExists("contract-decline")) { - response = httpUtil.getForbiddenResponse("This contract was declined! Please request a new one"); - return httpUtil.buildResponse(response, httpResponse); - } - - if (status.historyExists("negotiation-canceled")) { - response = httpUtil.getForbiddenResponse("This negotiation has already been canceled! Please request a new one"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if there is a contract available - if (!status.historyExists("contract-dataset")) { - response = httpUtil.getBadRequest("No contract is available!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if the contract id is correct - History history = status.getHistory("contract-dataset"); - if (!history.getId().contains(contractId)) { - response = httpUtil.getBadRequest("This contract id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - // Check the validity of the token - String expectedToken = processManager.generateToken(process, history.getId()); - String token = tokenRequestBody.getToken(); - if (!expectedToken.equals(token)) { - response = httpUtil.getForbiddenResponse("The token is invalid!"); - return httpUtil.buildResponse(response, httpResponse); - } - if (status.getStatus().equals("COMPLETED") || status.getStatus().equals("RETRIEVED") || status.historyExists("transfer-request") || status.historyExists("transfer-completed") || status.historyExists("data-received") || status.historyExists("data-retrieved")) { - response = httpUtil.getForbiddenResponse("This negotiation can not be canceled! It was already transferred!"); - return httpUtil.buildResponse(response, httpResponse); - } - String metaFile = null; - try { - metaFile = processManager.cancelProcess(httpRequest, processId); - } catch (Exception e) { - response.message = "This negotiation can not be canceled! The process has already finished!"; - return httpUtil.buildResponse(response, httpResponse); - } - if (metaFile == null) { - response.message = "Failed to cancel the negotiation!"; - return httpUtil.buildResponse(response, httpResponse); - } - - LogUtil.printStatus("[PROCESS " + processId + "] Negotiation [" + contractId + "] was canceled!"); - - response = httpUtil.getResponse("The negotiation was canceled!"); - response.data = processManager.getStatus(processId); - return httpUtil.buildResponse(response, httpResponse); + return contractService.cancelCall(httpRequest, httpResponse, tokenRequestBody); } catch (Exception e) { response.message = e.getMessage(); return httpUtil.buildResponse(response, httpResponse); @@ -568,125 +232,21 @@ public Response cancel(@Valid @RequestBody TokenRequest tokenRequestBody) { @RequestMapping(value = "/agree", method = RequestMethod.POST) @Operation(summary = "Start the negotiation for an specific asset, contract agreement and policy agreement") public Response negotiate(@Valid @RequestBody TokenRequest tokenRequestBody) { - Long signedAt = DateTimeUtil.getTimestamp(); Response response = httpUtil.getInternalError(); - - // Check for authentication - if (!authService.isAuthenticated(httpRequest)) { - response = httpUtil.getNotAuthorizedResponse(); - return httpUtil.buildResponse(response, httpResponse); - } try { - // Check for the mandatory fields - List mandatoryParams = List.of("processId", "contractId", "token"); - if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { - response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check for processId - String processId = tokenRequestBody.getProcessId(); - if (!processManager.checkProcess(httpRequest, processId)) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - - Process process = processManager.getProcess(httpRequest, processId); - if (process == null) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Get status to check for contract id - String contractId = tokenRequestBody.getContractId(); - Status status = processManager.getStatus(processId); - - // Check if was already declined - if (status.historyExists("contract-decline")) { - response = httpUtil.getForbiddenResponse("This contract was declined! Please request a new one"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if negotiation is canceled - if (status.historyExists("negotiation-canceled")) { - response = httpUtil.getForbiddenResponse("This negotiation has been canceled! Please request a new one"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if was already agreed - if (status.historyExists("contract-agreed")) { - response = httpUtil.getForbiddenResponse("This contract is already agreed!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if there is a contract available - if (!status.historyExists("contract-dataset")) { - response = httpUtil.getBadRequest("No contract is available!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if the contract id is correct - History history = status.getHistory("contract-dataset"); - if (!history.getId().contains(contractId)) { - response = httpUtil.getBadRequest("This contract id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Load all the available contracts - Map availableContracts = processManager.loadDatasets(processId); - String seedId = String.join("|",availableContracts.keySet()); // Generate Seed - // Check the validity of the token - String expectedToken = processManager.generateToken(process, seedId); - String token = tokenRequestBody.getToken(); - if (!expectedToken.equals(token)) { - response = httpUtil.getForbiddenResponse("The token is invalid!"); - return httpUtil.buildResponse(response, httpResponse); - } - Dataset dataset = availableContracts.get(contractId); - - if (dataset == null) { - response.message = "The Contract Selected was not found!"; - return httpUtil.buildResponse(response, httpResponse); - } - - String policyId = tokenRequestBody.getPolicyId(); - Set policy = null; - DataTransferService.NegotiateContract contractNegotiation = null; - policy = policyUtil.getPolicyById(dataset, policyId); - if (policy == null) { - response = httpUtil.getBadRequest("The policy selected does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - contractNegotiation = dataService - .new NegotiateContract( - processManager.loadDataModel(httpRequest), - processId, - status.getBpn(), - status.getProviderBpn(), - dataset, - processManager.getStatus(processId), - policy - ); - String statusPath = processManager.setAgreed(httpRequest, processId, signedAt, contractId, policyId); - if (statusPath == null) { - response.message = "Something went wrong when agreeing with the contract!"; + // Check for authentication + if (!authService.isAuthenticated(httpRequest)) { + response = httpUtil.getNotAuthorizedResponse(); return httpUtil.buildResponse(response, httpResponse); } - LogUtil.printMessage("[PROCESS " + processId + "] Contract [" + contractId + "] Agreed! Starting negotiation..."); - processManager.startNegotiation(httpRequest, processId, contractNegotiation); - LogUtil.printStatus("[PROCESS " + processId + "] Negotiation for [" + contractId + "] started!"); - - response = httpUtil.getResponse("The contract was agreed successfully! Negotiation started!"); - response.data = processManager.getStatus(processId); - return httpUtil.buildResponse(response, httpResponse); - + return contractService.doContractAgreement(httpRequest, httpResponse, tokenRequestBody); } catch (Exception e) { response.message = e.getMessage(); return httpUtil.buildResponse(response, httpResponse); } } + /** * HTTP POST method to decline a Passport negotiation. *

@@ -705,75 +265,12 @@ public Response decline(@Valid @RequestBody TokenRequest tokenRequestBody) { return httpUtil.buildResponse(response, httpResponse); } try { - // Check for the mandatory fields - List mandatoryParams = List.of("processId", "token"); - if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { - response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check for processId - String processId = tokenRequestBody.getProcessId(); - if (!processManager.checkProcess(httpRequest, processId)) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - - Process process = processManager.getProcess(httpRequest, processId); - if (process == null) { - response = httpUtil.getBadRequest("The process id does not exists!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Get status to check for contract id - Status status = processManager.getStatus(processId); - - // Check if was already declined - if (status.historyExists("contract-decline")) { - response = httpUtil.getForbiddenResponse("This contract has already been declined!"); - return httpUtil.buildResponse(response, httpResponse); - } - - if (status.historyExists("negotiation-canceled")) { - response = httpUtil.getForbiddenResponse("This negotiation has been canceled! Please request a new one"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if there is a contract available - if (!status.historyExists("contract-dataset")) { - response = httpUtil.getBadRequest("No contract is available!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Check if the contract id is correct - Map availableContracts = processManager.loadDatasets(processId); - String seedId = String.join("|",availableContracts.keySet()); // Generate Seed - // Check the validity of the token - String expectedToken = processManager.generateToken(process, seedId); - String token = tokenRequestBody.getToken(); - if (!expectedToken.equals(token)) { - response = httpUtil.getForbiddenResponse("The token is invalid!"); - return httpUtil.buildResponse(response, httpResponse); - } - - // Decline contract - String statusPath = processManager.setDecline(httpRequest, processId); - if (statusPath == null) { - response.message = "Something went wrong when declining the contract!"; - return httpUtil.buildResponse(response, httpResponse); - } - - LogUtil.printMessage("[PROCESS " + processId + "] Contracts declined!"); - response = httpUtil.getResponse("The contract negotiation was successfully declined"); - return httpUtil.buildResponse(response, httpResponse); - + return contractService.declineContractAgreement(httpRequest, httpResponse, tokenRequestBody); } catch (Exception e) { response.message = e.getMessage(); return httpUtil.buildResponse(response, httpResponse); } - } } diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/models/http/requests/SingleApiRequest.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/models/http/requests/SingleApiRequest.java new file mode 100644 index 000000000..e7dd40183 --- /dev/null +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/models/http/requests/SingleApiRequest.java @@ -0,0 +1,128 @@ +/********************************************************************************* + * + * Tractus-X - Digital Product Passport Application + * + * Copyright (c) 2022, 2024 BMW AG, Henkel AG & Co. KGaA + * Copyright (c) 2023, 2024 CGI Deutschland B.V. & Co. KG + * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the + * License for the specific language govern in permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.digitalproductpass.models.http.requests; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; + + +/** + * This class consists exclusively to define attributes related to the many endpoint request like "/sign", "/passport", etc. + * It's the mandatory body parameter for the HTTP requests. + **/ +public class SingleApiRequest { + + /** ATTRIBUTES **/ + @NotNull(message = "serialized id needs to be defined!") + @JsonProperty(value="id") + String id; + + @JsonProperty(value="idType", defaultValue = "partInstanceId") + String idType; + + @NotNull(message = "Dtr search id needs to be defined!") + @JsonProperty("discoveryId") + String discoveryId; + + @JsonProperty(value="discoveryIdType", defaultValue = "manufacturerPartId") + String discoveryIdType; + + @JsonProperty("children") + Boolean children; + + @JsonProperty("semanticId") + String semanticId; + + /** CONSTRUCTOR(S) **/ + public SingleApiRequest(String id, String idType, String discoveryId, String discoveryIdType, Boolean children, String semanticId) { + this.id = id; + this.idType = idType; + this.discoveryId = discoveryId; + this.discoveryIdType = discoveryIdType; + this.children = children; + this.semanticId = semanticId; + } + + public SingleApiRequest() { + } + + public SingleApiRequest(String id, String discoveryId) { + this.id = id; + this.discoveryId = discoveryId; + } + + + /** GETTERS AND SETTERS **/ + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getIdType() { + return idType; + } + + public void setIdType(String idType) { + this.idType = idType; + } + + public String getDiscoveryId() { + return discoveryId; + } + + public String getDiscoveryIdType() { + return discoveryIdType; + } + + + public Boolean getChildren() { + return children; + } + + public void setChildren(Boolean children) { + this.children = children; + } + + public String getSemanticId() { + return semanticId; + } + + public void setSemanticId(String semanticId) { + this.semanticId = semanticId; + } + + public void setDiscoveryId(String discoveryId) { + this.discoveryId = discoveryId; + } + + public void setDiscoveryIdType(String discoveryIdType) { + this.discoveryIdType = discoveryIdType; + } +} diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/AuthenticationService.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/AuthenticationService.java index 34924393c..f29d89600 100644 --- a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/AuthenticationService.java +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/AuthenticationService.java @@ -198,6 +198,27 @@ public Boolean tokenContainsAnyRole(Jwt jwtToken){ } } + /** + * Checks if the user is Api Key authenticated. + *

+ * @param httpRequest + * the {@code HttpServletRequest} object representing the HTTP request. + * + * @return true if the user is authenticated, false otherwise. + * + */ + public Boolean isApiKeyAuthenticated(HttpServletRequest httpRequest) { + try { + final String xApiKey = httpRequest.getHeader(securityConfig.getAuthentication().getHeader()); + String vaultApiKey = vaultService.getLocalSecret("oauth.apiKey").toString(); + if (vaultApiKey == null || vaultApiKey.isEmpty()) + throw new ServiceException(this.getClass().getName()+"."+"isApiKeyAuthenticated", "API key is null or empty!"); + return vaultApiKey.equals(xApiKey); + } catch (Exception e) { + throw new ServiceException(this.getClass().getName()+"."+"isApiKeyAuthenticated", e, "There was a error when checking for the api key authentication!"); + } + } + /** * Checks if the user is authenticated. *

diff --git a/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/ContractService.java b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/ContractService.java new file mode 100644 index 000000000..4b029a299 --- /dev/null +++ b/dpp-backend/digitalproductpass/src/main/java/org/eclipse/tractusx/digitalproductpass/services/ContractService.java @@ -0,0 +1,875 @@ +/********************************************************************************* + * + * Tractus-X - Digital Product Passport Application + * + * Copyright (c) 2022, 2024 BMW AG, Henkel AG & Co. KGaA + * Copyright (c) 2023, 2024 CGI Deutschland B.V. & Co. KG + * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation + * + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the + * License for the specific language govern in permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.digitalproductpass.services; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.tractusx.digitalproductpass.config.DiscoveryConfig; +import org.eclipse.tractusx.digitalproductpass.config.DtrConfig; +import org.eclipse.tractusx.digitalproductpass.config.PassportConfig; +import org.eclipse.tractusx.digitalproductpass.exceptions.ControllerException; +import org.eclipse.tractusx.digitalproductpass.exceptions.ServiceException; +import org.eclipse.tractusx.digitalproductpass.exceptions.ServiceInitializationException; +import org.eclipse.tractusx.digitalproductpass.managers.DtrSearchManager; +import org.eclipse.tractusx.digitalproductpass.managers.ProcessManager; +import org.eclipse.tractusx.digitalproductpass.models.catenax.BpnDiscovery; +import org.eclipse.tractusx.digitalproductpass.models.catenax.Dtr; +import org.eclipse.tractusx.digitalproductpass.models.edc.AssetSearch; +import org.eclipse.tractusx.digitalproductpass.models.http.Response; +import org.eclipse.tractusx.digitalproductpass.models.http.requests.DiscoverySearch; +import org.eclipse.tractusx.digitalproductpass.models.http.requests.Search; +import org.eclipse.tractusx.digitalproductpass.models.http.requests.TokenRequest; +import org.eclipse.tractusx.digitalproductpass.models.manager.History; +import org.eclipse.tractusx.digitalproductpass.models.manager.Process; +import org.eclipse.tractusx.digitalproductpass.models.manager.SearchStatus; +import org.eclipse.tractusx.digitalproductpass.models.manager.Status; +import org.eclipse.tractusx.digitalproductpass.models.negotiation.Dataset; +import org.eclipse.tractusx.digitalproductpass.models.negotiation.Set; +import org.eclipse.tractusx.digitalproductpass.models.service.BaseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import utils.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +/** + * This class consists exclusively of methods to operate on executing the Data Plane operations. + * + *

The methods defined here are intended to do every needed operations in order to be able to transfer data or passport from Data Plane Endpoint. + * + */ +@Service +public class ContractService extends BaseService { + + /** ATTRIBUTES **/ + private @Autowired DataTransferService dataService; + private @Autowired AasService aasService; + private @Autowired PassportConfig passportConfig; + private @Autowired DiscoveryConfig discoveryConfig; + private @Autowired DtrConfig dtrConfig; + private @Autowired EdcUtil edcUtil; + @Autowired + ProcessManager processManager; + @Autowired + DtrSearchManager dtrSearchManager; + @Autowired + CatenaXService catenaXService; + @Autowired + HttpUtil httpUtil; + private @Autowired JsonUtil jsonUtil; + + /** CONSTRUCTOR(S) **/ + public ContractService() throws ServiceInitializationException { + this.checkEmptyVariables(); + } + + /** METHODS **/ + + /** + * Creates an empty variables List. + *

+ * + * @return an empty {@code Arraylist}. + * + */ + @Override + public List getEmptyVariables() { + return new ArrayList<>(); + } + + /** + * Creates a new process and checks for the viability of the data retrieval + *

+ * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param searchBody + * the {@code DiscoverySearch} request body. + * + * @return a {@code Response} response object of the method. + * + * @throws ServiceException + * if unable to create the process. + */ + public Response createCall (HttpServletResponse httpResponse, DiscoverySearch searchBody) throws ServiceException { + try { + Response response = httpUtil.getInternalError(); + try { + // If the discovery id type is not defined use the default specified in the configuration + if(searchBody.getType() == null || searchBody.getType().isEmpty()) { + searchBody.setType(this.discoveryConfig.getBpn().getKey()); // Set default configuration key as default + } + List mandatoryParams = List.of("id"); + if (!jsonUtil.checkJsonKeys(searchBody, mandatoryParams, ".", false)) { + response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); + return httpUtil.buildResponse(response, httpResponse); + } + + List bpnDiscoveries = null; + try { + bpnDiscoveries = catenaXService.getBpnDiscovery(searchBody.getId(), searchBody.getType()); + } catch (Exception e) { + response.message = "Failed to get the bpn number from the discovery service"; + response.status = 404; + response.statusText = "Not Found"; + return httpUtil.buildResponse(response, httpResponse); + } + if (bpnDiscoveries == null || bpnDiscoveries.size() == 0) { + response.message = "Failed to get the bpn number from the discovery service, discovery response is null"; + response.status = 404; + response.statusText = "Not Found"; + return httpUtil.buildResponse(response, httpResponse); + } + List bpnList = new ArrayList<>(); + for (BpnDiscovery bpnDiscovery : bpnDiscoveries) { + bpnList.addAll(bpnDiscovery.mapBpnNumbers()); + } + if (bpnList.size() == 0) { + response.message = "The asset was not found in the BPN Discovery!"; + response.status = 404; + response.statusText = "Not Found"; + return httpUtil.buildResponse(response, httpResponse); + } + String processId = processManager.initProcess(); + LogUtil.printMessage("Creating process [" + processId + "] for " + searchBody.getType() + ": " + searchBody.getId()); + ConcurrentHashMap> dataModel = null; + if (dtrConfig.getTemporaryStorage().getEnabled()) { + try { + dataModel = this.dtrSearchManager.loadDataModel(); + } catch (Exception e) { + LogUtil.printWarning("Failed to load data model from disk!"); + } + } + // This checks if the cache is deactivated or if the bns are not in the dataModel, if one of them is not in the data model then we need to check for them + if (!dtrConfig.getTemporaryStorage().getEnabled() || ((dataModel == null) || !jsonUtil.checkJsonKeys(dataModel, bpnList, ".", false))) { + catenaXService.searchDTRs(bpnList, processId); + } else { + boolean requestDtrs = false; + // Take the results from cache + for (String bpn : bpnList) { + List dtrs = null; + try { + dtrs = (List) jsonUtil.bindReferenceType(dataModel.get(bpn), new TypeReference>() { + }); + } catch (Exception e) { + throw new ControllerException(this.getClass().getName(), e, "Could not bind the reference type!"); + } + + if (dtrs == null || dtrs.size() == 0) { + continue; + } + Long currentTimestamp = DateTimeUtil.getTimestamp(); + // Iterate over every DTR and add it to the file + for (Dtr dtr : dtrs) { + + Long validUntil = dtr.getValidUntil(); + //Check if invalid time has come + if (dtr.getInvalid() && validUntil > currentTimestamp) { + continue; + } + + if (dtr.getContractId() == null || dtr.getContractId().isEmpty() || validUntil == null || validUntil < currentTimestamp) { + requestDtrs = true; // If the cache invalidation time has come request Dtrs + break; + } + + processManager.addSearchStatusDtr(processId, dtr); //Add valid DTR to status + } + + // If the refresh from the cache is necessary + if (requestDtrs) { + dtrSearchManager.deleteBpns(dataModel, bpnList); // Delete BPN numbers + catenaXService.searchDTRs(bpnList, processId); // Start again the search + break; + } + } + } + + SearchStatus status = processManager.getSearchStatus(processId); + if (status == null) { + return httpUtil.buildResponse(httpUtil.getNotFound("It was not possible to search for the decentral digital twin registries"), httpResponse); + } + if (status.getDtrs().isEmpty()) { + return httpUtil.buildResponse(httpUtil.getNotFound("No decentral digital twin registry was found"), httpResponse); + } + response = httpUtil.getResponse(); + response.data = Map.of( + "processId", processId + ); + return httpUtil.buildResponse(response, httpResponse); + } catch (Exception e) { + assert response != null; + response.message = "It was not possible to create the process and search for the decentral digital twin registries"; + LogUtil.printException(e, response.message); + return httpUtil.buildResponse(response, httpResponse); + } + } catch (Exception e){ + throw new ServiceException(this.getClass().getName()+"."+"createCall", + e, + "It was not possible to create the process!"); + } + } + + /** + * Searches the passport of an asset. + *

+ * @param httpRequest + * the {@code HttpServletRequest} the http request object + * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param searchBody + * the {@code Search} request body. + * + * @return a {@code Response} response object of the method. + * + * @throws ServiceException + * if unable to find any digital twin. + */ + public Response searchCall(HttpServletRequest httpRequest, HttpServletResponse httpResponse, Search searchBody) throws ServiceException { + try { + Response response = httpUtil.getInternalError(); + List mandatoryParams = List.of("id"); + if (!jsonUtil.checkJsonKeys(searchBody, mandatoryParams, ".", false)) { + response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); + return httpUtil.buildResponse(response, httpResponse); + } + + Process process = null; + AssetSearch assetSearch = null; + + // Check for processId + if(searchBody.getProcessId() == null){ + response = httpUtil.getBadRequest("No processId was found on the request body!"); + return httpUtil.buildResponse(response, httpResponse); + } + // If the id type is not defined use the default specified in the configuration + if(searchBody.getIdType() == null || searchBody.getIdType().isEmpty()){ + searchBody.setIdType(this.passportConfig.getDefaultIdType()); + } + + String processId = searchBody.getProcessId(); + if(processId.isEmpty()){ + response = httpUtil.getBadRequest("Process id is required for decentral digital twin registry searches!"); + return httpUtil.buildResponse(response, httpResponse); + } + SearchStatus searchStatus = processManager.getSearchStatus(processId); + if (searchStatus == null) { + response = httpUtil.getBadRequest("The searchStatus id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + if(searchStatus.getDtrs().keySet().size() == 0){ + response = httpUtil.getBadRequest("No digital twins are available for this process!"); + return httpUtil.buildResponse(response, httpResponse); + } + Boolean childrenCondition = searchBody.getChildren(); + String logPrint = "[PROCESS " + processId + "] Creating search for "+searchBody.getIdType() + ": "+ searchBody.getId(); + if(childrenCondition != null){ + LogUtil.printMessage(logPrint + " with drilldown enabled"); + process = processManager.createProcess(processId, childrenCondition, httpRequest); // Store the children condition + }else { + LogUtil.printMessage(logPrint + " with drilldown disabled"); + process = processManager.createProcess(processId, httpRequest); + } + Status status = processManager.getStatus(processId); + if (status == null) { + response = httpUtil.getBadRequest("The status is not available!"); + return httpUtil.buildResponse(response, httpResponse); + } + assetSearch = aasService.decentralDtrSearch(process.id, searchBody); + + + if(assetSearch == null){ + LogUtil.printWarning("[PROCESS " + processId + "] The decentralized digital twin registry asset search return a null response!"); + status = processManager.getStatus(processId); + // Here start the algorithm to refresh the dtrs in the cache if the transfer was incompleted + List dtrList = new ArrayList(); + Map dtrs = searchStatus.getDtrs(); + List bpnList = new ArrayList(); + for(String dtrId: searchStatus.getDtrs().keySet()){ + // Check if any dtr search was incomplete + if(!status.historyExists("dtr-"+dtrId+"-transfer-incomplete")) { + continue; + } + // Add the dtr bpn to the update cache list + Dtr dtr = dtrs.get(dtrId); + String bpn = dtr.getBpn(); + if(!bpnList.contains(bpn)) { + bpnList.add(dtr.getBpn()); // Add bpn to delete in the cache + } + dtrList.add(dtr); + } + + // If no bpn numbers need to be updated is because there is no digital twin found + if(bpnList.size() == 0){ + response = httpUtil.getBadRequest("No digital twin was found!"); + return httpUtil.buildResponse(response, httpResponse); + } + + LogUtil.printWarning("["+dtrList.size()+"] Digital Twin Registries Contracts are invalid and need to be refreshed! For the following BPN Number(s): "+ bpnList.toString()); + // Refresh cache or search id + if(dtrConfig.getTemporaryStorage().getEnabled()) { + ConcurrentHashMap> dataModel = null; + try { + dataModel = this.dtrSearchManager.loadDataModel(); + } catch (Exception e) { + LogUtil.printWarning("Failed to load data model from disk!"); + } + dtrSearchManager.deleteBpns(dataModel, bpnList); // Delete BPN numbers + } + LogUtil.printMessage("Refreshing ["+bpnList.size()+"] BPN Number Endpoints..."); + catenaXService.searchDTRs(bpnList, processId); // Start again the search for refreshing the dtrs + assetSearch = aasService.decentralDtrSearch(process.id, searchBody); // Start again the search + if(assetSearch == null) { // If again was not found then we give an error + response = httpUtil.getBadRequest("No digital twin was found! Even after retrying the digital twin transfer!"); + return httpUtil.buildResponse(response, httpResponse); + } + } + // Assing the variables with the content + String assetId = assetSearch.getAssetId(); + String connectorAddress = assetSearch.getConnectorAddress(); + + /*[1]=========================================*/ + // Get catalog with all the contract offers + if(connectorAddress == null){ + LogUtil.printError("The connector address is empty!"); + } + if(assetId == null){ + LogUtil.printError("The assetId is empty!"); + } + Catalog catalog = null; + Map datasets = null; + Long startedTime = DateTimeUtil.getTimestamp(); + try { + catalog = dataService.getContractOfferCatalog(connectorAddress, assetId); + datasets = edcUtil.filterValidContracts(dataService.getContractOffers(catalog), this.passportConfig.getPolicyCheck()); + } catch (ServiceException e) { + LogUtil.printError("The EDC is not reachable, it was not possible to retrieve catalog! Trying again..."); + catalog = dataService.getContractOfferCatalog(connectorAddress, assetId); + datasets = edcUtil.filterValidContracts(dataService.getContractOffers(catalog), this.passportConfig.getPolicyCheck()); + if (datasets == null) { // If the contract catalog is not reachable retry... + response.message = "The EDC is not reachable, it was not possible to retrieve catalog! Please try again!"; + response.status = 502; + response.statusText = "Service Not Available"; + return httpUtil.buildResponse(response, httpResponse); + } + } + // Check if contract offer was not received + if (datasets == null) { + // Retry again... + LogUtil.printWarning("[PROCESS " + process.id + "] No asset id found for the dataset contract offers in the catalog! Requesting catalog again..."); + catalog = dataService.getContractOfferCatalog(connectorAddress, assetId); + datasets = edcUtil.filterValidContracts(dataService.getContractOffers(catalog), this.passportConfig.getPolicyCheck()); + if (datasets == null) { // If the contract catalog is not reachable retry... + response.message = "Asset Id not found in any contract!"; + response.status = 404; + response.statusText = "Not Found"; + return httpUtil.buildResponse(response, httpResponse); + } + } + processManager.setProviderBpn(processId, catalog.getParticipantId()); + String seedId = String.join("|", datasets.keySet()); + LogUtil.printDebug("[PROCESS " + process.id + "] ["+datasets.size()+"] Contracts found for asset [" + assetId + "] in EDC Endpoint [" + connectorAddress + "]"); + + response = null; + response = httpUtil.getResponse(); + response.data = Map.of( + "id", process.id, + "contracts", datasets, + "token", processManager.generateToken(process, seedId) + ); + + processManager.saveDatasets(process.id, datasets, seedId, startedTime, false); + // After the search is performed the search dir is deleted + if(!processManager.deleteSearchDir(process.id)){ + LogUtil.printError("It was not possible to delete the search.json file for the process"); + }else{ + LogUtil.printDebug("[PROCESS " + process.id + "] The tmp search directory was deleted successfully!"); + } + + return httpUtil.buildResponse(response, httpResponse); + } catch (Exception e){ + throw new ServiceException(this.getClass().getName()+"."+"searchCall", + e, + "It was not possible to find digital twins!"); + } + } + + /** + * Does the contract agreement for a contract negotiation + *

+ * @param httpRequest + * the {@code HttpServletRequest} the http request object + * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param tokenRequestBody + * the {@code TokenRequest} token request body. + * + * @return a {@code Response} response object of the method + * + * @throws ServiceException + * if unable to do the contract agreement + */ + public Response doContractAgreement(HttpServletRequest httpRequest, HttpServletResponse httpResponse, TokenRequest tokenRequestBody) throws ServiceException { + try { + Response response = httpUtil.getInternalError(); + Long signedAt = DateTimeUtil.getTimestamp(); + + // Check for the mandatory fields + List mandatoryParams = List.of("processId", "contractId", "token"); + if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { + response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); + return httpUtil.buildResponse(response, httpResponse); + } + // Check for processId + String processId = tokenRequestBody.getProcessId(); + if (!processManager.checkProcess(httpRequest, processId)) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + + Process process = processManager.getProcess(httpRequest, processId); + if (process == null) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Get status to check for contract id + String contractId = tokenRequestBody.getContractId(); + Status status = processManager.getStatus(processId); + + // Check if was already declined + if (status.historyExists("contract-decline")) { + response = httpUtil.getForbiddenResponse("This contract was declined! Please request a new one"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if negotiation is canceled + if (status.historyExists("negotiation-canceled")) { + response = httpUtil.getForbiddenResponse("This negotiation has been canceled! Please request a new one"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if was already agreed + if (status.historyExists("contract-agreed")) { + response = httpUtil.getForbiddenResponse("This contract is already agreed!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if there is a contract available + if (!status.historyExists("contract-dataset")) { + response = httpUtil.getBadRequest("No contract is available!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if the contract id is correct + History history = status.getHistory("contract-dataset"); + if (!history.getId().contains(contractId)) { + response = httpUtil.getBadRequest("This contract id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Load all the available contracts + Map availableContracts = processManager.loadDatasets(processId); + String seedId = String.join("|", availableContracts.keySet()); // Generate Seed + // Check the validity of the token + String expectedToken = processManager.generateToken(process, seedId); + String token = tokenRequestBody.getToken(); + if (!expectedToken.equals(token)) { + response = httpUtil.getForbiddenResponse("The token is invalid!"); + return httpUtil.buildResponse(response, httpResponse); + } + Dataset dataset = availableContracts.get(contractId); + + if (dataset == null) { + response.message = "The Contract Selected was not found!"; + return httpUtil.buildResponse(response, httpResponse); + } + + String policyId = tokenRequestBody.getPolicyId(); + DataTransferService.NegotiateContract contractNegotiation = null; + // Check if policy is available! + if (policyId != null) { + Set policy = edcUtil.getPolicyById(dataset, policyId); + if (policy == null) { + response = httpUtil.getBadRequest("The policy selected does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + contractNegotiation = dataService + .new NegotiateContract( + processManager.loadDataModel(httpRequest), + processId, + status.getBpn(), + dataset, + processManager.getStatus(processId), + policy + ); + } else { + // If the policy is not selected get the first one by default + contractNegotiation = dataService + .new NegotiateContract( + processManager.loadDataModel(httpRequest), + processId, + status.getBpn(), + dataset, + processManager.getStatus(processId) + ); + } + String statusPath = processManager.setAgreed(httpRequest, processId, signedAt, contractId, policyId); + if (statusPath == null) { + response.message = "Something went wrong when agreeing with the contract!"; + return httpUtil.buildResponse(response, httpResponse); + } + LogUtil.printMessage("[PROCESS " + processId + "] Contract [" + contractId + "] Agreed! Starting negotiation..."); + processManager.startNegotiation(httpRequest, processId, contractNegotiation); + LogUtil.printStatus("[PROCESS " + processId + "] Negotiation for [" + contractId + "] started!"); + + response = httpUtil.getResponse("The contract was agreed successfully! Negotiation started!"); + response.data = processManager.getStatus(processId); + return httpUtil.buildResponse(response, httpResponse); + } catch (Exception e){ + throw new ServiceException(this.getClass().getName()+"."+"doContractAgreement", + e, + "It was not possible to do the contract agreement!"); + } + } + + /** + * Cancels the contract negotiation. + *

+ * @param httpRequest + * the {@code HttpServletRequest} the http request object + * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param tokenRequestBody + * the {@code TokenRequest} token request body. + * + * @return a {@code Response} response object of the method + * + * @throws ServiceException + * if unable to cancel the contract. + */ + public Response cancelCall(HttpServletRequest httpRequest, HttpServletResponse httpResponse, TokenRequest tokenRequestBody) { + try { + Response response = httpUtil.getInternalError(); + // Check for the mandatory fields + List mandatoryParams = List.of("processId", "contractId", "token"); + if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { + response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check for processId + String processId = tokenRequestBody.getProcessId(); + if (!processManager.checkProcess(httpRequest, processId)) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + + Process process = processManager.getProcess(httpRequest, processId); + if (process == null) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Get status to check for contract id + String contractId = tokenRequestBody.getContractId(); + Status status = processManager.getStatus(processId); + + // Check if was already declined + if (status.historyExists("contract-decline")) { + response = httpUtil.getForbiddenResponse("This contract was declined! Please request a new one"); + return httpUtil.buildResponse(response, httpResponse); + } + + if (status.historyExists("negotiation-canceled")) { + response = httpUtil.getForbiddenResponse("This negotiation has already been canceled! Please request a new one"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if there is a contract available + if (!status.historyExists("contract-dataset")) { + response = httpUtil.getBadRequest("No contract is available!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if the contract id is correct + History history = status.getHistory("contract-dataset"); + if (!history.getId().contains(contractId)) { + response = httpUtil.getBadRequest("This contract id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + // Check the validity of the token + String expectedToken = processManager.generateToken(process, history.getId()); + String token = tokenRequestBody.getToken(); + if (!expectedToken.equals(token)) { + response = httpUtil.getForbiddenResponse("The token is invalid!"); + return httpUtil.buildResponse(response, httpResponse); + } + if (status.getStatus().equals("COMPLETED") || status.getStatus().equals("RETRIEVED") || status.historyExists("transfer-request") || status.historyExists("transfer-completed") || status.historyExists("data-received") || status.historyExists("data-retrieved")) { + response = httpUtil.getForbiddenResponse("This negotiation can not be canceled! It was already transferred!"); + return httpUtil.buildResponse(response, httpResponse); + } + String metaFile = null; + try { + metaFile = processManager.cancelProcess(httpRequest, processId); + } catch (Exception e) { + response.message = "This negotiation can not be canceled! The process has already finished!"; + return httpUtil.buildResponse(response, httpResponse); + } + if (metaFile == null) { + response.message = "Failed to cancel the negotiation!"; + return httpUtil.buildResponse(response, httpResponse); + } + + LogUtil.printStatus("[PROCESS " + processId + "] Negotiation [" + contractId + "] was canceled!"); + + response = httpUtil.getResponse("The negotiation was canceled!"); + response.data = processManager.getStatus(processId); + return httpUtil.buildResponse(response, httpResponse); + } catch (Exception e){ + throw new ServiceException(this.getClass().getName()+"."+"cancelCall", + e, + "It was not possible to cancel the contract!"); + } + } + + /** + * Declines a contract agreement. + *

+ * @param httpRequest + * the {@code HttpServletRequest} the http request object + * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param tokenRequestBody + * the {@code TokenRequest} token request body. + * + * @return a {@code Response} response object of the method + * + * @throws ServiceException + * if unable to decline the contract agreement. + */ + public Response declineContractAgreement(HttpServletRequest httpRequest, HttpServletResponse httpResponse, TokenRequest tokenRequestBody) { + try { + Response response = httpUtil.getInternalError(); + // Check for the mandatory fields + List mandatoryParams = List.of("processId", "token"); + if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { + response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check for processId + String processId = tokenRequestBody.getProcessId(); + if (!processManager.checkProcess(httpRequest, processId)) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + Process process = processManager.getProcess(httpRequest, processId); + if (process == null) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Get status to check for contract id + Status status = processManager.getStatus(processId); + + // Check if was already declined + if (status.historyExists("contract-decline")) { + response = httpUtil.getForbiddenResponse("This contract has already been declined!"); + return httpUtil.buildResponse(response, httpResponse); + } + + if (status.historyExists("negotiation-canceled")) { + response = httpUtil.getForbiddenResponse("This negotiation has been canceled! Please request a new one"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if there is a contract available + if (!status.historyExists("contract-dataset")) { + response = httpUtil.getBadRequest("No contract is available!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if the contract id is correct + Map availableContracts = processManager.loadDatasets(processId); + String seedId = String.join("|",availableContracts.keySet()); // Generate Seed + // Check the validity of the token + String expectedToken = processManager.generateToken(process, seedId); + String token = tokenRequestBody.getToken(); + if (!expectedToken.equals(token)) { + response = httpUtil.getForbiddenResponse("The token is invalid!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Decline contract + String statusPath = processManager.setDecline(httpRequest, processId); + if (statusPath == null) { + response.message = "Something went wrong when declining the contract!"; + return httpUtil.buildResponse(response, httpResponse); + } + + LogUtil.printMessage("[PROCESS " + processId + "] Contracts declined!"); + response = httpUtil.getResponse("The contract negotiation was successfully declined"); + return httpUtil.buildResponse(response, httpResponse); + } catch (Exception e){ + throw new ServiceException(this.getClass().getName()+"."+"cancelContractAgreement", + e, + "It was not possible to cancel the contract agreement!"); + } + + } + + /** + * Get the status from the process with the given processId + *

+ * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param processId + * the {@code String} process identification. + * + * @return a {@code Response} response object of the method. + * + */ + public Response statusCall(HttpServletResponse httpResponse, String processId) { + // Get status + Response response = httpUtil.getResponse(); + response.data = processManager.getStatus(processId); + return httpUtil.buildResponse(response, httpResponse); + } + + /** + * Retrieves the Passport data. + *

+ * @param httpRequest + * the {@code HttpServletRequest} the http request object + * @param httpResponse + * the {@code HttpServletResponse} the http response object + * @param tokenRequestBody + * the {@code TokenRequest} token request body. + * + * @return a {@code Response} response object of the method + * + * @throws ServiceException + * if unable to retrieve the passport. + */ + public Response getDataCall (HttpServletRequest httpRequest, HttpServletResponse httpResponse, TokenRequest tokenRequestBody) throws ServiceException { + try { + Response response = httpUtil.getInternalError(); + // Check for the mandatory fields + List mandatoryParams = List.of("processId", "contractId", "token"); + if (!jsonUtil.checkJsonKeys(tokenRequestBody, mandatoryParams, ".", false)) { + response = httpUtil.getBadRequest("One or all the mandatory parameters " + mandatoryParams + " are missing"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check for processId + String processId = tokenRequestBody.getProcessId(); + if (!processManager.checkProcess(httpRequest, processId)) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + Process process = processManager.getProcess(httpRequest, processId); + if (process == null) { + response = httpUtil.getBadRequest("The process id does not exists!"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Get status to check for contract id + String contractId = tokenRequestBody.getContractId(); + Status status = processManager.getStatus(processId); + + if (status.historyExists("contract-decline")) { + response = httpUtil.getForbiddenResponse("The contract for this passport has been declined!"); + return httpUtil.buildResponse(response, httpResponse); + } + if (status.historyExists("negotiation-canceled")) { + response = httpUtil.getForbiddenResponse("This negotiation has been canceled! Please request a new one"); + return httpUtil.buildResponse(response, httpResponse); + } + + // Check if the contract id is correct + Map availableContracts = processManager.loadDatasets(processId); + String seedId = String.join("|",availableContracts.keySet()); // Generate Seed + // Check the validity of the token + String expectedToken = processManager.generateToken(process, seedId); + String token = tokenRequestBody.getToken(); + if (!expectedToken.equals(token)) { + response = httpUtil.getForbiddenResponse("The token is invalid!"); + return httpUtil.buildResponse(response, httpResponse); + } + Dataset dataset = availableContracts.get(contractId); + + if (dataset == null) { + response.message = "The Contract Selected was not found!"; + return httpUtil.buildResponse(response, httpResponse); + } + + if (!status.historyExists("data-received")) { + status = processManager.getStatus(processId); // Retry to get the status before giving an error + if(!status.historyExists("data-received")) { + response = httpUtil.getNotFound("The data is not available!"); + return httpUtil.buildResponse(response, httpResponse); + } + } + + if (status.historyExists("data-retrieved")) { + response = httpUtil.getNotFound("The data was already retrieved and is no longer available!"); + return httpUtil.buildResponse(response, httpResponse); + } + String semanticId = status.getSemanticId(); + JsonNode passport = processManager.loadPassport(processId); + if (passport == null) { + response = httpUtil.getNotFound("Failed to load passport!"); + return httpUtil.buildResponse(response, httpResponse); + } + Map negotiation = processManager.loadNegotiation(processId); + Map transfer = processManager.loadTransfer(processId); + response = httpUtil.getResponse(); + response.data = Map.of( + "metadata", Map.of( + "contract", dataset, + "negotiation", negotiation, + "transfer", transfer + ), + "aspect", passport, + "semanticId", semanticId + ); + return httpUtil.buildResponse(response, httpResponse); + } catch (Exception e){ + throw new ServiceException(this.getClass().getName()+"."+"getDataCall", + e, + "It was not possible to retrieve the passport!"); + } + } +} diff --git a/dpp-backend/digitalproductpass/src/main/resources/application.yml b/dpp-backend/digitalproductpass/src/main/resources/application.yml index ab66f5f0a..91e6f08ee 100644 --- a/dpp-backend/digitalproductpass/src/main/resources/application.yml +++ b/dpp-backend/digitalproductpass/src/main/resources/application.yml @@ -67,6 +67,8 @@ configuration: startUpChecks: bpnCheck: false edcCheck: false + authentication: + header: "X-Api-Key" irs: enabled: true @@ -126,6 +128,9 @@ configuration: key: "bpn" timeout: 1500 + singleApi: + maxRetries: 30 + delay: 1000 process: store: true @@ -134,6 +139,7 @@ configuration: signKey: 'c55e3f35200f6afedbce37cefdaf40eadd15c92814edfdbc4d6ab0eacdcdd56dbcd5a2a34ca4b675084d33f9f479d7d79347795148aaf4443e1b47ab96b27e72' passport: + defaultIdType: "partInstanceId" searchIdSchema: "CX::" dataTransfer: encrypt: true @@ -176,6 +182,7 @@ configuration: - "client.secret" - "edc.apiKey" - "edc.participantId" + - "oauth.apiKey" server: error: