From 0f8b8bbb9ed124e56c6ea54cc2204ed889a3acdb Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Wed, 5 Jun 2024 11:05:10 +0200 Subject: [PATCH] Rework and test (#63) * add issuer * add charts * initial local and test setup * doc * fix key * enable did-helper * add test workflow * tests and docs * more doc * more documentation * more doc * verify the results * deploy the chart * fix verification * stable version * update verifier chart * Update doc/LOCAL.MD Co-authored-by: Tim Smyth <33017641+pulledtim@users.noreply.github.com> * Update doc/LOCAL.MD Co-authored-by: Tim Smyth <33017641+pulledtim@users.noreply.github.com> * Update energyReport.json --------- Co-authored-by: Tim Smyth <33017641+pulledtim@users.noreply.github.com> --- .github/workflows/release-helm.yaml | 73 +- .github/workflows/test.yaml | 20 + README.md | 14 + charts/data-space-connector/Chart.yaml | 52 ++ .../templates/_helpers.tpl | 55 ++ .../templates/apisix-cm.yaml | 31 + .../templates/authentication-secrets.yaml | 14 + .../templates/data-plane-secrets.yaml | 13 + .../templates/database-secrets.yaml | 13 + .../templates/dataplane-registration.yaml | 39 ++ .../templates/did-key-deployment.yaml | 66 ++ .../templates/did-key-ingress.yaml | 21 + .../templates/did-key-service.yaml | 19 + .../templates/issuance-secrets.yaml | 15 + .../templates/opa-cm.yaml | 32 + .../templates/participant-registration.yaml | 23 + .../data-space-connector/templates/realm.yaml | 154 +++++ charts/data-space-connector/values.yaml | 632 ++++++++++++++++++ charts/trust-anchor/Chart.yaml | 13 + charts/trust-anchor/templates/_helpers.tpl | 55 ++ charts/trust-anchor/templates/secrets.yaml | 14 + charts/trust-anchor/values.yaml | 43 ++ doc/LOCAL.MD | 470 +++++++++++++ doc/img/components.drawio | 184 +++++ doc/img/components.png | Bin 0 -> 124843 bytes doc/img/consumer.drawio | 28 + doc/img/consumer.jpg | Bin 0 -> 14617 bytes doc/img/overview.drawio | 31 + doc/img/overview.jpg | Bin 0 -> 13265 bytes doc/img/provider.drawio | 85 +++ doc/img/provider.jpg | Bin 0 -> 52416 bytes generate.sh | 35 - it/pom.xml | 275 ++++++++ .../FancyMarketplaceEnvironment.java | 22 + .../it/components/KeycloakHelper.java | 32 + .../components/MPOperationsEnvironment.java | 37 + .../it/components/RunCucumberTest.java | 18 + .../it/components/StepDefinitions.java | 181 +++++ .../dataspace/it/components/TestUtils.java | 127 ++++ .../it/components/TrustAnchorEnvironment.java | 9 + .../dataspace/it/components/Wallet.java | 298 +++++++++ .../it/components/model/Credential.java | 18 + .../it/components/model/CredentialOffer.java | 29 + .../components/model/CredentialRequest.java | 22 + .../dataspace/it/components/model/Format.java | 81 +++ .../dataspace/it/components/model/Grant.java | 20 + .../components/model/IssuerConfiguration.java | 32 + .../it/components/model/OfferUri.java | 19 + .../components/model/OpenIdConfiguration.java | 40 ++ .../model/SupportedConfiguration.java | 26 + .../it/components/model/TokenResponse.java | 25 + .../model/VerifiablePresentation.java | 26 + it/src/test/resources/it/mvds_basic.feature | 9 + .../test/resources/policies/energyReport.json | 37 + k3s/consumer.yaml | 281 ++++++++ k3s/infra/coredns/coredns-cm.yaml | 39 ++ k3s/infra/coredns/deployment.yaml | 128 ++++ k3s/infra/traefik/deployment.yaml | 62 ++ k3s/infra/traefik/ingress.yaml | 19 + k3s/infra/traefik/rbac.yaml | 57 ++ k3s/infra/traefik/service.yaml | 30 + k3s/namespaces/consumer.yaml | 4 + k3s/namespaces/infra.yaml | 4 + k3s/namespaces/provider.yaml | 4 + k3s/namespaces/trust-anchor.yaml | 4 + k3s/namespaces/wallet.yaml | 4 + k3s/provider.yaml | 193 ++++++ k3s/trust-anchor.yaml | 19 + pom.xml | 317 +++++++++ 69 files changed, 4690 insertions(+), 102 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 charts/data-space-connector/Chart.yaml create mode 100644 charts/data-space-connector/templates/_helpers.tpl create mode 100644 charts/data-space-connector/templates/apisix-cm.yaml create mode 100644 charts/data-space-connector/templates/authentication-secrets.yaml create mode 100644 charts/data-space-connector/templates/data-plane-secrets.yaml create mode 100644 charts/data-space-connector/templates/database-secrets.yaml create mode 100644 charts/data-space-connector/templates/dataplane-registration.yaml create mode 100644 charts/data-space-connector/templates/did-key-deployment.yaml create mode 100644 charts/data-space-connector/templates/did-key-ingress.yaml create mode 100644 charts/data-space-connector/templates/did-key-service.yaml create mode 100644 charts/data-space-connector/templates/issuance-secrets.yaml create mode 100644 charts/data-space-connector/templates/opa-cm.yaml create mode 100644 charts/data-space-connector/templates/participant-registration.yaml create mode 100644 charts/data-space-connector/templates/realm.yaml create mode 100644 charts/data-space-connector/values.yaml create mode 100644 charts/trust-anchor/Chart.yaml create mode 100644 charts/trust-anchor/templates/_helpers.tpl create mode 100644 charts/trust-anchor/templates/secrets.yaml create mode 100644 charts/trust-anchor/values.yaml create mode 100644 doc/LOCAL.MD create mode 100644 doc/img/components.drawio create mode 100644 doc/img/components.png create mode 100644 doc/img/consumer.drawio create mode 100644 doc/img/consumer.jpg create mode 100644 doc/img/overview.drawio create mode 100644 doc/img/overview.jpg create mode 100644 doc/img/provider.drawio create mode 100644 doc/img/provider.jpg delete mode 100755 generate.sh create mode 100644 it/pom.xml create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/FancyMarketplaceEnvironment.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/KeycloakHelper.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/MPOperationsEnvironment.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/RunCucumberTest.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/StepDefinitions.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/TestUtils.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/TrustAnchorEnvironment.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/Wallet.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/Credential.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/CredentialOffer.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/CredentialRequest.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/Format.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/Grant.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/IssuerConfiguration.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/OfferUri.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/OpenIdConfiguration.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/SupportedConfiguration.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/TokenResponse.java create mode 100644 it/src/test/java/org/fiware/dataspace/it/components/model/VerifiablePresentation.java create mode 100644 it/src/test/resources/it/mvds_basic.feature create mode 100644 it/src/test/resources/policies/energyReport.json create mode 100644 k3s/consumer.yaml create mode 100644 k3s/infra/coredns/coredns-cm.yaml create mode 100644 k3s/infra/coredns/deployment.yaml create mode 100644 k3s/infra/traefik/deployment.yaml create mode 100644 k3s/infra/traefik/ingress.yaml create mode 100644 k3s/infra/traefik/rbac.yaml create mode 100644 k3s/infra/traefik/service.yaml create mode 100644 k3s/namespaces/consumer.yaml create mode 100644 k3s/namespaces/infra.yaml create mode 100644 k3s/namespaces/provider.yaml create mode 100644 k3s/namespaces/trust-anchor.yaml create mode 100644 k3s/namespaces/wallet.yaml create mode 100644 k3s/provider.yaml create mode 100644 k3s/trust-anchor.yaml create mode 100644 pom.xml diff --git a/.github/workflows/release-helm.yaml b/.github/workflows/release-helm.yaml index 8c61505..331ad75 100644 --- a/.github/workflows/release-helm.yaml +++ b/.github/workflows/release-helm.yaml @@ -7,52 +7,6 @@ on: jobs: - generate-version: - runs-on: ubuntu-latest - - outputs: - version: ${{ steps.out.outputs.version }} - - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-java@v1 - with: - java-version: '17' - java-package: jdk - - - id: pr - uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Match semver label via bash - id: match-label-bash - run: | - LABELS=$(cat <<-END - ${{ steps.pr.outputs.labels }} - END - ) - IFS='\n' read -ra LABEL <<< "$LABELS" - for i in "${LABEL[@]}"; do - case $i in - # Will just use the first occurence - 'major'|'minor'|'patch') - echo "RELEASE_LABEL=$i" >> $GITHUB_OUTPUT - break - esac - done - - - uses: zwaldowski/semver-release-action@v2 - with: - dry_run: true - bump: ${{ steps.match-label-bash.outputs.RELEASE_LABEL }} - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Set version output - id: out - run: echo "::set-output name=version::$(echo ${VERSION})" - deploy: needs: [ "generate-version" ] @@ -68,33 +22,18 @@ jobs: run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - # See https://github.com/helm/chart-releaser-action/issues/6 + - name: Install Helm run: | curl -fsSLo get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh - # prepare yaml parser - - uses: actions/setup-go@v4 - - name: Install yq - run: | - go install github.com/mikefarah/yq/v4@latest - yq --version - - - name: Generate Chart.yaml - run: | - ./generate.sh ${{ needs.generate-version.outputs.version }} - - - - name: Install releaser - run: | - wget https://github.com/helm/chart-releaser/releases/download/v1.6.0/chart-releaser_1.6.0_linux_amd64.tar.gz - tar -xvzf chart-releaser_1.6.0_linux_amd64.tar.gz - ./cr package charts/data-space-connector - ./cr upload --owner ${GITHUB_REPOSITORY_OWNER} --git-repo data-space-connector --packages-with-index --token ${{ secrets.GITHUB_TOKEN }} --push --skip-existing - ./cr index --owner ${GITHUB_REPOSITORY_OWNER} --git-repo data-space-connector --packages-with-index --index-path . --token ${{ secrets.GITHUB_TOKEN }} --push + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.5.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_SKIP_EXISTING: true git-release: needs: ["generate-version","deploy"] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..aae09b3 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,20 @@ +name: Test +on: + push + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-java@v1 + with: + java-version: '17' + java-package: jdk + + - name: Execute tests + id: test + run: | + mvn clean integration-test -Ptest \ No newline at end of file diff --git a/README.md b/README.md index 7178e3a..11d5e17 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ FIWARE [data-space-connector repository](https://github.com/FIWARE/data-space-co ## Deployment +### Local Deployment + +The FIWARE Data Space Connector provides a local deployment of a Minimal Viable Dataspace. +Find a detailed documentation here: [Local Deployment](./doc/LOCAL.MD) ### Deployment with ArgoCD @@ -75,3 +79,13 @@ The chart is [generated](generate.sh) on each merge to master from the current a Different examples for the deployment of the FIWARE Data Space connector can be found under the [./examples](./examples) directory. + +## Testing + +In order to test the [helm-charts](./charts/) provided for the FIWARE Data Space Connector, an integration-test framework based on [Cucumber](https://cucumber.io/) and [Junit5](https://junit.org/junit5/) is provided: [it](./it). + +The tests can be executed via: +```shell + mvn clean integration-test -Ptest +``` +They will spin up the [Local Data Space](./doc/LOCAL.MD) and run the [test-scenarios](./it/src/test/resources/it/mvds_basic.feature) against it. \ No newline at end of file diff --git a/charts/data-space-connector/Chart.yaml b/charts/data-space-connector/Chart.yaml new file mode 100644 index 0000000..1a025e3 --- /dev/null +++ b/charts/data-space-connector/Chart.yaml @@ -0,0 +1,52 @@ +apiVersion: v2 +name: data-space-connector +description: Umbrella Chart for the FIWARE Data Space Connector, combining all essential parts to be used by a participant. +type: application +version: 3.0.0 +dependencies: + - name: postgresql + condition: postgresql.enabled + repository: oci://registry-1.docker.io/bitnamicharts + version: 13.1.5 + # authentication + - name: vcverifier + condition: vcverifier.enabled + version: 2.7.0 + repository: https://fiware.github.io/helm-charts + - name: credentials-config-service + condition: credentials-config-service.enabled + version: 0.1.5 + repository: https://fiware.github.io/helm-charts + - name: trusted-issuers-list + condition: trusted-issuers-list.enabled + version: 0.6.2 + repository: https://fiware.github.io/helm-charts + - name: mysql + condition: mysql.enabled + version: 9.4.4 + repository: https://charts.bitnami.com/bitnami + # authorization + - name: odrl-pap + condition: odrl-pap.enabled + version: 0.0.22 + repository: https://fiware.github.io/helm-charts + - name: apisix + condition: apisix.enabled + version: 3.1.0 + repository: oci://registry-1.docker.io/bitnamicharts + # data-service + - name: scorpio-broker-aaio + alias: scorpio + condition: scorpio.enabled + repository: https://fiware.github.io/helm-charts + version: 0.4.7 + - name: postgresql + alias: postgis + condition: postgis.enabled + repository: oci://registry-1.docker.io/bitnamicharts + version: 13.1.5 + # issuance + - name: keycloak + condition: keycloak.enabled + version: 21.1.1 + repository: https://charts.bitnami.com/bitnami \ No newline at end of file diff --git a/charts/data-space-connector/templates/_helpers.tpl b/charts/data-space-connector/templates/_helpers.tpl new file mode 100644 index 0000000..62dd817 --- /dev/null +++ b/charts/data-space-connector/templates/_helpers.tpl @@ -0,0 +1,55 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "dsc.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "dsc.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "dsc.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "dsc.serviceAccountName" -}} +{{- if .Values.did.serviceAccount.create -}} + {{ default (include "dsc.fullname" .) .Values.did.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.did.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "dsc.labels" -}} +app.kubernetes.io/name: {{ include "dsc.name" . }} +helm.sh/chart: {{ include "dsc.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} diff --git a/charts/data-space-connector/templates/apisix-cm.yaml b/charts/data-space-connector/templates/apisix-cm.yaml new file mode 100644 index 0000000..e765966 --- /dev/null +++ b/charts/data-space-connector/templates/apisix-cm.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: apisix-routes + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + apisix.yaml: |- + routes: + {{- if .Values.apisix.catchAllRoute.enabled }} + - uri: /* + upstream: + nodes: + {{ .Values.apisix.catchAllRoute.upstream.url}}: 1 + type: roundrobin + plugins: + openid-connect: + client_id: {{ .Values.apisix.catchAllRoute.oidc.clientId }} + client_secret: the-secret + bearer_only: true + use_jwks: true + discovery: {{ .Values.apisix.catchAllRoute.oidc.discoveryEndpoint }} + opa: + host: "http://localhost:{{ .Values.opa.port }}" + policy: policy/main + {{- end }} + {{- if .Values.apisix.routes }} + {{ .Values.apisix.routes | nindent 6 }} + {{- end }} + #END \ No newline at end of file diff --git a/charts/data-space-connector/templates/authentication-secrets.yaml b/charts/data-space-connector/templates/authentication-secrets.yaml new file mode 100644 index 0000000..dcc7150 --- /dev/null +++ b/charts/data-space-connector/templates/authentication-secrets.yaml @@ -0,0 +1,14 @@ +{{- if .Values.authentication.generatePasswords.enabled }} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ .Values.authentication.generatePasswords.secretName }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + mysql-root-password: {{ randAlphaNum 30 | b64enc | quote }} + mysql-replication-password: {{ randAlphaNum 30 | b64enc | quote }} + mysql-password: {{ randAlphaNum 30 | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/data-plane-secrets.yaml b/charts/data-space-connector/templates/data-plane-secrets.yaml new file mode 100644 index 0000000..3ff3f46 --- /dev/null +++ b/charts/data-space-connector/templates/data-plane-secrets.yaml @@ -0,0 +1,13 @@ +{{- if .Values.dataplane.generatePasswords.enabled }} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ .Values.dataplane.generatePasswords.secretName }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + postgres-user-password: {{ randAlphaNum 30 | b64enc | quote }} + postgres-admin-password: {{ randAlphaNum 30 | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/database-secrets.yaml b/charts/data-space-connector/templates/database-secrets.yaml new file mode 100644 index 0000000..d1e29f7 --- /dev/null +++ b/charts/data-space-connector/templates/database-secrets.yaml @@ -0,0 +1,13 @@ +{{- if .Values.postgresql.generatePasswords.enabled }} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ .Values.postgresql.generatePasswords.secretName }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + postgres-user-password: {{ randAlphaNum 30 | b64enc | quote }} + postgres-admin-password: {{ randAlphaNum 30 | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/dataplane-registration.yaml b/charts/data-space-connector/templates/dataplane-registration.yaml new file mode 100644 index 0000000..26b9e42 --- /dev/null +++ b/charts/data-space-connector/templates/dataplane-registration.yaml @@ -0,0 +1,39 @@ +{{- if and (eq .Values.scorpio.enabled true) (.Values.scorpio.ccs) }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.scorpio.ccs.configMap }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{- include "dsc.labels" . | nindent 4 }} +data: + init.sh: |- + # credentials config service registration + curl -X 'POST' \ + '{{ .Values.scorpio.ccs.endpoint }}/service' \ + -H 'accept: */*' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": {{ .Values.scorpio.ccs.id | quote }}, + "defaultOidcScope": {{ .Values.scorpio.ccs.defaultOidcScope.name | quote }}, + {{- if and (.Values.scorpio.ccs.defaultOidcScope.credentialType) (.Values.scorpio.ccs.defaultOidcScope.trustedParticipantsLists) (.Values.scorpio.ccs.defaultOidcScope.trustedIssuersLists) -}} + "oidcScopes": { + {{ .Values.scorpio.ccs.defaultOidcScope.name | quote }}: [ + { + "type": {{ .Values.scorpio.ccs.defaultOidcScope.credentialType | quote }}, + "trustedParticipantsLists": [ + {{ .Values.scorpio.ccs.defaultOidcScope.trustedParticipantsLists | quote }} + ], + "trustedIssuersLists": [ + {{ .Values.scorpio.ccs.defaultOidcScope.trustedIssuersLists | quote }} + ] + } + ] + } + {{- end }} + {{- if .Values.scorpio.ccs.oidcScopes -}} + "oidcScopes": {{- toJson .Values.scorpio.ccs.oidcScopes }} + {{- end }} + }' + +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/did-key-deployment.yaml b/charts/data-space-connector/templates/did-key-deployment.yaml new file mode 100644 index 0000000..484ad41 --- /dev/null +++ b/charts/data-space-connector/templates/did-key-deployment.yaml @@ -0,0 +1,66 @@ +{{- if .Values.did.enabled }} +kind: Deployment +apiVersion: apps/v1 +metadata: + name: did-helper + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + app.kubernetes.io/name: did-helper + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: did-helper + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + serviceAccountName: default + initContainers: + - name: init-did + image: quay.io/wi_stefan/did-helper:0.1.1 + env: + - name: COUNTRY + value: {{ .Values.did.cert.country }} + - name: STATE + value: {{ .Values.did.cert.state }} + - name: LOCALITY + value: {{ .Values.did.cert.locality }} + - name: ORGANIZATION + value: {{ .Values.did.cert.organization }} + - name: COMMON_NAME + value: {{ .Values.did.cert.commonName }} + - name: STORE_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.did.secret }} + key: store-pass + - name: KEY_ALIAS + value: didPrivateKey + - name: OUTPUT_FORMAT + value: env + - name: OUTPUT_FILE + value: /cert/did.env + volumeMounts: + - name: did-material + mountPath: /cert + + containers: + - name: did-material + imagePullPolicy: Always + image: "lipanski/docker-static-website:2.1.0" + ports: + - name: http + containerPort: 3000 + protocol: TCP + volumeMounts: + - name: did-material + mountPath: /home/static/did-material + volumes: + - name: did-material + emptyDir: { } +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/did-key-ingress.yaml b/charts/data-space-connector/templates/did-key-ingress.yaml new file mode 100644 index 0000000..a56cf9b --- /dev/null +++ b/charts/data-space-connector/templates/did-key-ingress.yaml @@ -0,0 +1,21 @@ +{{- if .Values.did.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: did-helper + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +spec: + rules: + - host: {{ .Values.did.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: did-helper + port: + name: http +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/did-key-service.yaml b/charts/data-space-connector/templates/did-key-service.yaml new file mode 100644 index 0000000..4e10e52 --- /dev/null +++ b/charts/data-space-connector/templates/did-key-service.yaml @@ -0,0 +1,19 @@ +{{- if .Values.did.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: did-helper + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +spec: + type: {{ .Values.did.serviceType }} + ports: + - port: {{ .Values.did.port }} + targetPort: 3000 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: did-helper + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/issuance-secrets.yaml b/charts/data-space-connector/templates/issuance-secrets.yaml new file mode 100644 index 0000000..6bab983 --- /dev/null +++ b/charts/data-space-connector/templates/issuance-secrets.yaml @@ -0,0 +1,15 @@ +{{- if .Values.issuance.generatePasswords.enabled }} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ .Values.issuance.generatePasswords.secretName }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + postgres-user-password: {{ randAlphaNum 30 | b64enc | quote }} + postgres-admin-password: {{ randAlphaNum 30 | b64enc | quote }} + keycloak-admin: {{ randAlphaNum 30 | b64enc | quote }} + store-pass: {{ randAlphaNum 30 | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/opa-cm.yaml b/charts/data-space-connector/templates/opa-cm.yaml new file mode 100644 index 0000000..5a2979b --- /dev/null +++ b/charts/data-space-connector/templates/opa-cm.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-config + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + opa.yaml: |- + services: + - name: bundle-server + url: {{ .Values.opa.resourceUrl }} + bundles: + policies: + service: bundle-server + resource: policies.tar.gz + polling: + min_delay_seconds: {{ .Values.opa.policies.minDelay }} + max_delay_seconds: {{ .Values.opa.policies.maxDelay }} + methods: + service: bundle-server + resource: methods.tar.gz + polling: + min_delay_seconds: {{ .Values.opa.methods.minDelay }} + max_delay_seconds: {{ .Values.opa.methods.maxDelay }} + data: + service: bundle-server + resource: data.tar.gz + polling: + min_delay_seconds: {{ .Values.opa.data.minDelay }} + max_delay_seconds: {{ .Values.opa.data.maxDelay }} + default_decision: /policy/main/allow \ No newline at end of file diff --git a/charts/data-space-connector/templates/participant-registration.yaml b/charts/data-space-connector/templates/participant-registration.yaml new file mode 100644 index 0000000..f0718f2 --- /dev/null +++ b/charts/data-space-connector/templates/participant-registration.yaml @@ -0,0 +1,23 @@ +{{- if .Values.registration.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.registration.configMap }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{- include "dsc.labels" . | nindent 4 }} +data: + init.sh: |- + # credentials config service registration + curl -X 'POST' \ + '{{ .Values.registration.til }}/issuer' \ + -H 'accept: */*' \ + -H 'Content-Type: application/json' \ + -d "{ + \"did\": \"{{ .Values.registration.did }}\", + \"credentials\": { + \"credentialsType\": \"{{ .Values.registration.credentialsType }}\" + } + }" + +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/templates/realm.yaml b/charts/data-space-connector/templates/realm.yaml new file mode 100644 index 0000000..9a623e5 --- /dev/null +++ b/charts/data-space-connector/templates/realm.yaml @@ -0,0 +1,154 @@ +{{- if .Values.keycloak.realm.import }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.keycloak.realm.name }}-realm + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "dsc.labels" . | nindent 4 }} +data: + {{ .Values.keycloak.realm.name }}-realm.json: |- + { + "id": "{{ .Values.keycloak.realm.name }}", + "realm": "{{ .Values.keycloak.realm.name }}", + "displayName": "Keycloak", + "displayNameHtml": "
Keycloak
", + "enabled": true, + "attributes": { + "frontendUrl": "{{ .Values.keycloak.realm.frontendUrl }}", + "issuerDid": "${DID}" + }, + "sslRequired": "none", + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges", + "composite": false, + "clientRole": false, + "containerId": "dome", + "attributes": {} + } + ], + "client": { + {{ .Values.keycloak.realm.clientRoles | nindent 10 }} + } + }, + "groups": [ + ], + "users": [ + {{ .Values.keycloak.realm.users | nindent 8 }} + ], + "clients": [ + {{ .Values.keycloak.realm.clients | nindent 8 }} + ], + "clientScopes": [ + { + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + ], + "defaultOptionalClientScopes": [ + ], + "components": { + "org.keycloak.protocol.oid4vc.issuance.signing.VerifiableCredentialsSigningService": [ + { + "id": "jwt-signing", + "name": "jwt-signing-service", + "providerId": "jwt_vc", + "subComponents": {}, + "config": { + "keyId": [ + "${DID}" + ], + "algorithmType": [ + "ES256" + ], + "issuerDid": [ + "${DID}" + ], + "tokenType": [ + "JWT" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "a4589e8f-7f82-4345-b2ea-ccc9d4366600", + "name": "test-key", + "providerId": "java-keystore", + "subComponents": {}, + "config": { + "keystore": [ "/did-material/cert.pfx" ], + "keystorePassword": [ "${STORE_PASS}" ], + "keyAlias": [ "didPrivateKey" ], + "keyPassword": [ "${STORE_PASS}" ], + "kid": [ "${DID}"], + "active": [ + "true" + ], + "priority": [ + "0" + ], + "enabled": [ + "true" + ], + "algorithm": [ + "ES256" + ] + } + } + ] + } + } + +{{- end }} \ No newline at end of file diff --git a/charts/data-space-connector/values.yaml b/charts/data-space-connector/values.yaml new file mode 100644 index 0000000..3d20bd3 --- /dev/null +++ b/charts/data-space-connector/values.yaml @@ -0,0 +1,632 @@ +# -- configuration to be shared between the authentication components +authentication: + generatePasswords: + # -- should a password for the database connection of authentication components be generated in the cluster + enabled: true + #-- name of the secret to put the generated password into + secretName: authentication-database-secret + +# -- configuration to be shared between the dataplane components +dataplane: + generatePasswords: + # -- should a password for the database connection of dataplane components be generated in the cluster + enabled: true + #-- name of the secret to put the generated password into + secretName: data-service-secret + +# -- configuration to be shared between the issuance components +issuance: + generatePasswords: + # -- should a password for the database connection of issuance components be generated in the cluster + enabled: true + #-- name of the secret to put the generated password into + secretName: issuance-secret + +# -- configuration for the mysql to be deployed as part of the connector, see https://github.com/bitnami/charts/tree/main/bitnami/mysql for all options +mysql: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: authentication-mysql + # -- configure authentication to mysql + auth: + # -- name of the secret to take the passowrds from + existingSecret: authentication-database-secret + # -- scripts to be executed on db startup + initdbScripts: + create.sql: | + CREATE DATABASE tildb; + CREATE DATABASE ccsdb; + +# -- configuration for the trusted-issuers-list to be deployed as part of the connector, see https://github.com/FIWARE/helm-charts/tree/main/charts/trusted-issuers-list for all options +trusted-issuers-list: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: trusted-issuers-list + # -- connection to the database + database: + # -- should persistence be used? + persistence: true + # -- name of the db user + username: root + # -- configuration for the existing secret to get the passwords from + existingSecret: + enabled: true + name: authentication-database-secret + key: mysql-root-password + # -- host of the database + host: authentication-mysql + # -- name of the schema inside the db + name: tildb + +# -- configuration for the vcverifier to be deployed as part of the connector, see https://github.com/FIWARE/helm-charts/tree/main/charts/vcverifier for all options +vcverifier: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: verifier + # -- configuration for the m2m flow, in case the tir is requiring authentication + m2m: + # -- we do not need authentication here + authEnabled: false + +# -- configuration for the credentials-config-service to be deployed as part of the connector, see https://github.com/FIWARE/helm-charts/tree/main/charts/credentials-config-service for all options +credentials-config-service: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: credentials-config-service + # -- connection to the database + database: + # -- should persistence be used? + persistence: true + # -- name of the db user + username: root + # -- configuration for the existing secret to get the passwords from + existingSecret: + enabled: true + name: authentication-database-secret + key: mysql-root-password + # -- host of the database + host: authentication-mysql + # -- name of the schema inside the db + name: ccsdb + +# -- configuration for the postgresql to be deployed as part of the connector, see https://github.com/bitnami/charts/tree/main/bitnami/postgresql for all options +postgresql: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: postgresql + generatePasswords: + # -- should a password for the database be generated in the cluster + enabled: true + # -- name of the secret to store the password in + secretName: database-secret + # -- configure authentication to mysql + auth: + # -- name of the secret to take the passowrds from + existingSecret: database-secret + # -- key of the secrets inside the secret + secretKeys: + adminPasswordKey: postgres-admin-password + userPasswordKey: postgres-user-password + # -- configuration for the primary of the db + primary: + # -- scripts to be run on intialization + initdb: + scripts: + create.sh: | + psql postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432 -c "CREATE DATABASE pap;" + psql postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432 -c "CREATE DATABASE keycloak;" + +# -- configuration for the odrl-pap to be deployed as part of the connector, see https://github.com/FIWARE/helm-charts/tree/main/charts/odrl-pap for all options +odrl-pap: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: odrl-pap + # -- configuration about the image to be used + deployment: + image: + repository: quay.io/fiware/odrl-pap + tag: 0.1.0 + pullPolicy: Always + # -- connection to the database + database: + # -- url to connect the db at + url: jdbc:postgresql://postgresql:5432/pap + # -- username to access the db + username: postgres + # -- secret to take the password from + existingSecret: + enabled: true + name: database-secret + key: postgres-admin-password + +# -- configuration for the open-policy-agent to be deployed as part of the connector, as a sidecar to apisix +opa: + # -- should an opa sidecar be deployed to apisix + enabled: true + # -- address of the pap to get the policies from + resourceUrl: http://odrl-pap:8080/bundles/service/v1 + # -- port to make opa available at + port: 8181 + # -- pull delays for the policies bundle + policies: + minDelay: 2 + maxDelay: 4 + # -- pull delays for the methods bundle + methods: + minDelay: 1 + maxDelay: 3 + # -- pull delays for the data bundle + data: + minDelay: 1 + maxDelay: 15 + +# -- configuration for apisix to be deployed as part of the connector, see https://github.com/bitnami/charts/tree/main/bitnami/apisix for all options +apisix: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- configuration in regard to the apisix control plane + controlPlane: + # -- should it be enabled + enabled: true + # -- configuration in regard to the apisix ingressController + ingressController: + # -- should it be enabled + enabled: false + # -- configuration in regard to the apisix etcd + etcd: + # -- should it be enabled + enabled: true + # -- persistence configuration of etcd + persistence: + # -- should it be enabled + enabled: false + # -- configuration in regard to the apisix dataplane + dataPlane: + # -- configuration for extra configmaps to be deployed + extraConfig: + deployment: + # -- allows to configure apisix through a yaml file + role_data_plane: + config_provider: yaml + # -- extra volumes + # we need `routes` to declaratively configure the routes + # and the config for the opa sidecar + extraVolumes: + - name: routes + configMap: + name: apisix-routes + - name: opa-config + configMap: + name: opa-config + # -- extra volumes to be mounted + extraVolumeMounts: + - name: routes + mountPath: /usr/local/apisix/conf/apisix.yaml + subPath: apisix.yaml + # -- sidecars to be deployed for apisix + sidecars: + # -- we want to deploy the open-policy-agent as a pep + - name: open-policy-agent + image: openpolicyagent/opa:0.64.1 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8181 + protocol: TCP + # opa should be started to listen at 8181 and get its config from the mounted config yaml + args: + - "run" + - "--ignore=.*" # exclude hidden dirs created by Kubernetes + - "--server" + - "-l" + - "debug" + - "-c" + - "/config/opa.yaml" + - "--addr" + - "0.0.0.0:8181" + volumeMounts: + - name: opa-config + mountPath: /config + # -- configuration of a catchAll-route(e.g. /*) + catchAllRoute: + # -- should it be enabled + enabled: true + # -- configuration to connect the upstream broker + upstream: + url: http://my-broker:8000 + # -- configuration to verify the jwt, coming from the verifier + oidc: + clientId: mySecuredService + discoveryEndpoint: http://verifier:3000/services/mySecuredService/.well-known/openid-configuration + + # -- configuration of routes for apisix + routes: +# - uri: /myRoute +# upstream: +# nodes: +# http://my-upstream-service:8080: 1 +# type: roundrobin +# plugins: +# openid-connect: +# client_id: test-id +# client_secret: the-secret +# bearer_only: true +# use_jwks: true +# discovery: http://the-service/.well-known/openid-configuration +# opa: +# host: "http://localhost:8181" +# policy: policy/main/allow + +# -- configuration for the postgresql to be deployed as part of the connector, see https://github.com/bitnami/charts/tree/main/bitnami/postgresql for all options +postgis: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- overrides the generated name, provides stable service names - this should be avoided if multiple instances are available in the same namespace + fullnameOverride: data-service-postgis + # -- overrides the generated name, provides stable service names - this should be avoided if multiple instances are available in the same namespace + nameOverride: data-service-postgis + ## auth configuration for the database + auth: + existingSecret: data-service-secret + secretKeys: + adminPasswordKey: postgres-admin-password + userPasswordKey: postgres-user-password + ## configuration of the postgres primary replicas + primary: + ## provide db initialization + initdb: + ## provide scripts for initialization + scripts: + # -- enable the postgis extension and create the database as expected by scorpio + enable.sh: | + psql postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432 -c "CREATE EXTENSION postgis;" + psql postgresql://postgres:${POSTGRES_PASSWORD}@localhost:5432 -c "CREATE DATABASE ngb;" + +## configuration of the context-broker - see https://github.com/FIWARE/helm-charts/tree/main/charts/scorpio-broker-aaio for details +scorpio: + fullnameOverride: data-service-scorpio + # -- should scorpio be enabled + enabled: true + ## configuration of the image to be used + image: + # -- repository to be used - resource friendly all-in-one-runner without kafka + repository: scorpiobroker/all-in-one-runner + # -- tag of the image to be used - latest java image without kafka + tag: java-4.1.11 + ## configuration of the database to be used by broker + db: + # -- host of the db + dbhost: data-service-postgis + # -- username to be used + user: postgres + existingSecret: + enabled: true + name: data-service-secret + key: postgres-admin-password + ## configuration of the readiness probe + readinessProbe: + # -- path to be used for the readiness probe, older versions used /actuator/health + path: /q/health + ## configuration of the liveness probe + livenessProbe: + # -- path to be used for the readiness probe, older versions used /actuator/health + path: /q/health + ## configuration to be used for the service offered by scorpio + service: + # -- ClusterIP is the recommended type for most clusters + type: ClusterIP + # -- configuration to register the dataplane at the credentials-config-service + ccs: + # -- endpoint of the ccs to regsiter at + endpoint: http://credentials-config-service:8080 + # -- configmap to get the registration information from + configMap: scorpio-registration + # -- service id of the data-service to be used + id: data-service + # -- default scope to be created for the data plane + defaultOidcScope: + # -- name of the scope + name: default + # -- name of the default credential to be configured + credentialType: VerifiableCredential + # -- needs to be updated for the concrete dataspace + trustedParticipantsLists: http://tir.trust-anchor.org + trustedIssuersLists: http://trusted-issuers-list:8080 + # -- additional init containers to be used for the dataplane + initContainers: + # -- curl container to register at the credentials config service + - name: register-credential-config + image: quay.io/curl/curl:8.1.2 + command: [ "/bin/sh", "-c", "/bin/init.sh" ] + volumeMounts: + - name: scorpio-registration + mountPath: /bin/init.sh + subPath: init.sh + # -- additonal volumes to be mounted for the dataplane + additionalVolumes: + - name: scorpio-registration + configMap: + name: scorpio-registration + defaultMode: 0755 + +## configuration of the keycloak - see https://github.com/bitnami/charts/tree/main/bitnami/keycloak for details +keycloak: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- disable the security context, required by the current quarkus container, will be solved in the future chart versions of keycloak + containerSecurityContext: + enabled: false + # -- keycloak image to be used - set to preview version of 25.0.0, since no other is available yet + image: + registry: quay.io + # until 25 is released, we have to use a snapshot version + repository: wi_stefan/keycloak + tag: 25.0.0-PRE + pullPolicy: Always + command: + - /bin/bash + # -- we need the did of the participant here. when its generated with the did-helper, we have to get it first and replace inside the realm.json through env-vars + args: + - -ec + - | + #!/bin/sh + export $(cat /did-material/did.env) + /opt/keycloak/bin/kc.sh start --features oid4vc-vci --import-realm + service: + ports: + http: 8080 + # -- authentication config for keycloak + auth: + existingSecret: issuance-secret + passwordSecretKey: keycloak-admin + adminUser: keycloak-admin + # -- should the db be deployed as part of the keycloak chart + postgresql: + enabled: false + # -- host of the external db to be used + externalDatabase: + host: postgresql + + # -- the default init container is deactivated, since it conflicts with the non-bitnami image + enableDefaultInitContainers: false + + # -- extra volumes to be mounted + extraVolumeMounts: + - name: empty-dir + mountPath: /opt/keycloak/lib/quarkus + subPath: app-quarkus-dir + - name: qtm-temp + mountPath: /qtm-tmp + - name: did-material + mountPath: /did-material + - name: did-material + mountPath: "/etc/env" + readOnly: true + - name: realms + mountPath: /opt/keycloak/data/import + + extraVolumes: + - name: did-material + emptyDir: { } + - name: qtm-temp + emptyDir: { } + - name: realms + configMap: + name: test-realm-realm + + # -- extra env vars to be set. we require them at the moment, since some of the chart config mechanisms only work with the bitnami-image + extraEnvVars: + # indicates ssl is terminated at the edge + - name: KC_PROXY + value: "edge" + # point the transaction store to the (writeable!) empty volume + - name: QUARKUS_TRANSACTION_MANAGER_OBJECT_STORE_DIRECTORY + value: /qtm-tmp + # config for the db connection + - name: KC_DB_URL_HOST + value: postgresql + - name: KC_DB_URL_DATABASE + value: keycloak + - name: KC_DB_USERNAME + value: postgres + - name: KC_DB_PASSWORD + valueFrom: + secretKeyRef: + name: database-secret + key: postgres-admin-password + # password for reading the key store connected to the did + - name: STORE_PASS + valueFrom: + secretKeyRef: + name: issuance-secret + key: store-pass + # keycloak admin password + - name: KC_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: issuance-secret + key: keycloak-admin + + # -- init containers to be run with keycloak + initContainers: + # workaround required by the current quarkus distribution, to make keycloak working + - name: read-only-workaround + image: quay.io/wi_stefan/keycloak:25.0.0-PRE + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + cp -r /opt/keycloak/lib/quarkus/* /quarkus + volumeMounts: + - name: empty-dir + mountPath: /quarkus + subPath: app-quarkus-dir + + # retrieve all did material required for the realm and store it to a shared folder + - name: get-did + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + apt-get -y update; apt-get -y install wget + cd /did-material + wget http://did-helper:3000/did-material/cert.pfx + wget http://did-helper:3000/did-material/did.env + volumeMounts: + - name: did-material + mountPath: /did-material + + # register the issuer at the trusted issuers registry - will only work if that one is publicly accessible + - name: register-at-tir + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + source /did-material/did.env + apt-get -y update; apt-get -y install curl + curl -X 'POST' 'http://tir.trust-anchor.svc.cluster.local:8080/issuer' -H 'Content-Type: application/json' -d "{\"did\": \"${DID}\", \"credentials\": []}" + volumeMounts: + - name: did-material + mountPath: /did-material + + # -- configuration of the realm to be imported + realm: + # -- should the realm be imported + import: true + # -- name of the realm + name: test-realm + # -- frontend url to be used for the realm + frontendURL: http://localhost:8080 + # -- client roles to be imported - be aware the env vars can be used and will be replaced + clientRoles: | + "${DID}": [ + { + "name": "ADMIN", + "description": "Is allowed to do everything", + "clientRole": true + } + ] + # -- users to be imported - be aware the env vars can be used and will be replaced + users: | + { + "username": "admin-user", + "enabled": true, + "email": "admin@provider.org", + "firstName": "Test", + "lastName": "Admin", + "credentials": [ + { + "type": "password", + "value": "test" + } + ], + "clientRoles": { + "${DID}": [ + "ADMIN" + ], + "account": [ + "view-profile", + "manage-account" + ] + }, + "groups": [ + ] + } + # -- clients to be imported - be aware the env vars can be used and will be replaced + clients: | + { + "clientId": "${DID}", + "enabled": true, + "description": "Client to manage itself", + "surrogateAuthRequired": false, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "defaultRoles": [], + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "oid4vc", + "attributes": { + "client.secret.creation.time": "1675260539", + "vc.natural-person.format": "jwt_vc", + "vc.natural-person.scope": "NaturalPersonCredential", + "vc.verifiable-credential.format": "jwt_vc", + "vc.verifiable-credential.scope": "VerifiableCredential" + }, + "protocolMappers": [ + { + "name": "target-role-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-target-role-mapper", + "config": { + "subjectProperty": "roles", + "clientId": "${DID}", + "supportedCredentialTypes": "NaturalPersonCredential" + } + }, + { + "name": "target-vc-role-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-target-role-mapper", + "config": { + "subjectProperty": "roles", + "clientId": "${DID}", + "supportedCredentialTypes": "VerifiableCredential" + } + }, + { + "name": "context-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-context-mapper", + "config": { + "context": "https://www.w3.org/2018/credentials/v1", + "supportedCredentialTypes": "VerifiableCredential,NaturalPersonCredential" + } + }, + { + "name": "email-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-user-attribute-mapper", + "config": { + "subjectProperty": "email", + "userAttribute": "email", + "supportedCredentialTypes": "NaturalPersonCredential" + } + } + ], + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [], + "optionalClientScopes": [] + } + +# -- configuration for the did-helper, should only be used for demonstrational deployments, see https://github.com/wistefan/did-helper +did: + enabled: false + +# -- configuration for registering a participant at the til, will most probably only be used in demonstrational enviornments +registration: + enabled: false \ No newline at end of file diff --git a/charts/trust-anchor/Chart.yaml b/charts/trust-anchor/Chart.yaml new file mode 100644 index 0000000..cd9df7b --- /dev/null +++ b/charts/trust-anchor/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: trust-anchor +description: Umbrella Chart to provide a minimal trust anchor for a FIWARE Dataspace +version: 0.0.1 +dependencies: + - name: trusted-issuers-list + condition: trusted-issuers-list.enabled + version: 0.6.2 + repository: https://fiware.github.io/helm-charts + - name: mysql + condition: mysql.enabled + version: 9.4.4 + repository: https://charts.bitnami.com/bitnami \ No newline at end of file diff --git a/charts/trust-anchor/templates/_helpers.tpl b/charts/trust-anchor/templates/_helpers.tpl new file mode 100644 index 0000000..0ef5783 --- /dev/null +++ b/charts/trust-anchor/templates/_helpers.tpl @@ -0,0 +1,55 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "trust-anchor.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "trust-anchor.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "trust-anchor.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "trust-anchor.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "trust-anchor.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "trust-anchor.labels" -}} +app.kubernetes.io/name: {{ include "trust-anchor.name" . }} +helm.sh/chart: {{ include "trust-anchor.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} diff --git a/charts/trust-anchor/templates/secrets.yaml b/charts/trust-anchor/templates/secrets.yaml new file mode 100644 index 0000000..824960f --- /dev/null +++ b/charts/trust-anchor/templates/secrets.yaml @@ -0,0 +1,14 @@ +{{- if .Values.generatePasswords.enabled }} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ .Values.generatePasswords.secretName }} + namespace: {{ $.Release.Namespace | quote }} + labels: + {{ include "trust-anchor.labels" . | nindent 4 }} +data: + mysql-root-password: {{ randAlphaNum 30 | b64enc | quote }} + mysql-replication-password: {{ randAlphaNum 30 | b64enc | quote }} + mysql-password: {{ randAlphaNum 30 | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/trust-anchor/values.yaml b/charts/trust-anchor/values.yaml new file mode 100644 index 0000000..96632a5 --- /dev/null +++ b/charts/trust-anchor/values.yaml @@ -0,0 +1,43 @@ +# -- configuration to be shared between the trust-anchor components +generatePasswords: + # -- should a password for the database connection of trust-anchor components be generated in the cluster + enabled: true + #-- name of the secret to put the generated password into + secretName: mysql-database-secret + +# -- configuration for the mysql to be deployed as part of the trust-anchor, see https://github.com/bitnami/charts/tree/main/bitnami/mysql for all options +mysql: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: trust-anchor-mysql + # -- configure authentication to mysql + auth: + # -- name of the secret to take the passowrds from + existingSecret: mysql-database-secret + # -- scripts to be executed on db startup + initdbScripts: + create.sql: | + CREATE DATABASE tirdb; + +# -- configuration for the trusted-issuers-list to be deployed as part of the trust-anchor, see https://github.com/FIWARE/helm-charts/tree/main/charts/trusted-issuers-list for all options +trusted-issuers-list: + # -- should it be enabled? set to false if one outside the chart is used. + enabled: true + # -- allows to set a fixed name for the services + fullnameOverride: tir + # -- connection to the database + database: + # -- should persistence be used? + persistence: true + # -- name of the db user + username: root + # -- configuration for the existing secret to get the passwords from + existingSecret: + enabled: true + name: mysql-database-secret + key: mysql-root-password + # -- host of the database + host: trust-anchor-mysql + # -- name of the schema inside the db + name: tirdb diff --git a/doc/LOCAL.MD b/doc/LOCAL.MD new file mode 100644 index 0000000..ef3a113 --- /dev/null +++ b/doc/LOCAL.MD @@ -0,0 +1,470 @@ +# Local Deployment + +In order to support development and exploration of the FIWARE Data Space Connector, a "Minimal Viable Dataspace" is +provided as part of this repo. + +## Quick Start + +> :warning: The local deployment uses [k3s](https://k3s.io/) and is currently only tested on linux. + +To start the Data Space, just use: + +```shell + mvn clean deploy -Plocal +``` + +Depending on the machine, it should take between 5 and 10min to spin up the complete data space. You can connect to the +running k3s-cluster via: + +```shell + export KUBECONFIG=$(pwd)/target/k3s.yaml + # get all deployed resources + kubectl get all --all-namespaces +``` + +## The Data Space + +![Overview](./img/overview.jpg) + +The locally deployed Data Space consists of 2 Participants, connected through a Trust Anchor. + +### The Trust Anchor + +Every Data Spaces requires a framework that ensures trust between the participants. Depending on the requirements of the +concrete Data Space, +this can become a rather complex topic. Various trust-providers exist( +f.e. [Gaia-X Digital Clearing Houses](https://gaia-x.eu/gxdch)) and could be reused, as long as +they provide an implementation of +the [EBSI-Trusted Issuers Registry](https://hub.ebsi.eu/apis/pilot/trusted-issuers-registry/v4) to the participants. + +The local Data Spaces comes with the [FIWARE Trusted Issuers List](https://github.com/FIWARE/trusted-issuers-list) as a +rather simple implementation of that API, providing CRUD functionality +for Issuers and storage in an MySQL Database. After deployment, the API is available +at ```http://tir.127.0.0.1.nip.io:8080```. Both participants +are automatically registered as "Trusted Issuers" in the registry with their did's. + +Get a list of the issuers: + +```shell + curl -X GET http://tir.127.0.0.1.nip.io:8080/v4/issuers +``` + +A new issuer could for example be registered via: + +```shell + curl -X POST http://til.127.0.0.1.nip.io:8080/issuer \ + --header 'Content-Type: application/json' \ + --data '{ + "did": "did:key:myKey", + "credentials": [] + }' +``` + +For more information about the API, see +its [OpenAPI Spec](https://github.com/FIWARE/trusted-issuers-list/blob/main/api/trusted-issuers-list.yaml) + +## The Participants + +The minimal Data Space should provide an easy-to-understand introduction to the FIWARE Data Space. Therefor the roles of +the +participants are clearly seperated into "Data Consumer" and "Data Provider". However, in most real-world Data Spaces the +participants +will have both roles. They are not restricted to either consume or provide. + +In our scenario, the Data Provider(`M&P Operations Inc.`) is a company offering solutions to host and operate digital +services for other companies. The Data Consumer(`Fancy Marketplace Co.`) +provides a marketplace solution, listing offers from other companies. To fulfill their roles, they need different +components of the FIWARE Data Space Connector. + +### The Data Consumer + +![Consumer](./img/consumer.jpg) + +Since the Data Consumer in our example is only retrieving data, it requires very few components: + +* [Keycloak](https://github.com/keycloak/keycloak) - to issue VerifiableCredentials +* [did-helper](https://github.com/wistefan/did-helper) - a small helper application, providing the decentralized + identity to be used for the local Data Space + +After deployment, Keycloak can be used to issue VerifiableCredentials for users or services, to be used for +authorization at other participants of the Data Space. +It comes with 2 preconfigured users: + +* the `keycloak-admin` - has a password generated during deployment, it can be retrieved + via ```kubectl get secret -n consumer -o json issuance-secret | jq '.data."keycloak-admin"' -r | base64 --decode``` +* the `test-user` - it has a fixed password, set to "test" + +The admin-console of keycloak is available at: ```http://keycloak-consumer.127.0.0.1.nip.io:8080```, login with +the `keycloak-admin` +The credentials issuance in the account-console is available +at: ```http://keycloak-consumer.127.0.0.1.nip.io:8080/realms/test-realm/account/oid4vci```, login with the `test-user` + +In order to retrieve an actual credential two ways are available: + +* Use the account-console and retrieve the credential with a wallet. Currently, we cannot recommend any for a local use + case. +* Get the credential via http-requests through the `SameDevice-Flow`: + +> :warning: The pre-authorized code and the offer expire within 30s for security reasons. Be fast. + +> :bulb: In case you did the demo before, you can use the following snippet to unset the env-vars: +> ```shell +> unset ACCESS_TOKEN; unset OFFER_URI; unset PRE_AUTHORIZED_CODE; \ +> unset CREDENTIAL_ACCESS_TOKEN; unset VERIFIABLE_CREDENTIAL; unset HOLDER_DID; \ +> unset VERIFIABLE_PRESENTATION; unset JWT_HEADER; unset PAYLOAD; unset SIGNATURE; unset JWT; \ +> unset VP_TOKEN; unset DATA_SERVICE_ACCESS_TOKEN; +> ``` + +Get an AccessToken from Keycloak: + +```shell + export ACCESS_TOKEN=$(curl -s -X POST http://keycloak-consumer.127.0.0.1.nip.io:8080/realms/test-realm/protocol/openid-connect/token \ + --header 'Accept: */*' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data client_id=admin-cli \ + --data username=test-user \ + --data password=test | jq '.access_token' -r); echo ${ACCESS_TOKEN} +``` + +(Optional, since in the local case we know all of the values in advance) +Get the credentials issuer information: + +```shell + curl -X GET http://keycloak-consumer.127.0.0.1.nip.io:8080/realms/test-realm/.well-known/openid-credential-issuer +``` + +Get a credential offer uri(for the `user-credential), using the retrieved AccessToken: + +```shell + export OFFER_URI=$(curl -s -X GET 'http://keycloak-consumer.127.0.0.1.nip.io:8080/realms/test-realm/protocol/oid4vc/credential-offer-uri?credential_configuration_id=user-credential' \ + --header "Authorization: Bearer ${ACCESS_TOKEN}" | jq '"\(.issuer)\(.nonce)"' -r); echo ${OFFER_URI} +``` + +Use the offer uri(e.g. the `issuer`and `nonce` fields), to retrieve the actual offer: + +```shell + export PRE_AUTHORIZED_CODE=$(curl -s -X GET ${OFFER_URI} \ + --header "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.grants."urn:ietf:params:oauth:grant-type:pre-authorized_code"."pre-authorized_code"' -r); echo ${PRE_AUTHORIZED_CODE} +``` + +Exchange the pre-authorized code from the offer with an AccessToken at the authorization server: + +```shell + export CREDENTIAL_ACCESS_TOKEN=$(curl -s -X POST http://keycloak-consumer.127.0.0.1.nip.io:8080/realms/test-realm/protocol/openid-connect/token \ + --header 'Accept: */*' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code \ + --data code=${PRE_AUTHORIZED_CODE} | jq '.access_token' -r); echo ${CREDENTIAL_ACCESS_TOKEN} +``` + +Use the returned access token to get the actual credential: + +```shell + export VERIFIABLE_CREDENTIAL=$(curl -s -X POST http://keycloak-consumer.127.0.0.1.nip.io:8080/realms/test-realm/protocol/oid4vc/credential \ + --header 'Accept: */*' \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer ${CREDENTIAL_ACCESS_TOKEN}" \ + --data '{"credential_identifier":"user-credential", "format":"jwt_vc"}' | jq '.credential' -r); echo ${VERIFIABLE_CREDENTIAL} +``` + +You will receive a jwt-encoded credential to be used within the data space. + +### The Data Provider + +![Provider](./img/provider.jpg) + +The Data Provider requires a couple of more components, in order to provide secure access to its data. It needs +essentially 3 building blocks: + +* Data-Service: In our example case, the data is provided by + an [NGSI-LD Context Broker](https://github.com/ScorpioBroker/ScorpioBroker) +* Authentication: + * [VCVerifier](https://github.com/FIWARE/VCVerifier): Verifies incoming VerifiableCredentials + through [OID4VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) and returns a JWT token + * [CredentialsConfigService](https://github.com/FIWARE/credentials-config-service): Allows to configure the Trusted + Lists to be used for certain credentials + * [TrustedIssuersList](https://github.com/FIWARE/trusted-issuers-list): Allows to specify the capabilities of + certain issuers, f.e. what credentials they are allowed to issue +* Authorization: + * [Apisix](https://apisix.apache.org/): An Api-Gateway that verifies the incoming JWT(provided by the verifier) and + acts as Policy Enforcement Point to authorize requests based on the PDP's decision + * [Open Policy Agent](https://www.openpolicyagent.org/): Acts as the Policy Decision Point, to evaluate existing + policies for requests and either allows or denies them. + * [ODRL-PAP](https://github.com/wistefan/odrl-pap): Policy Administration & Information Point, that allows to + configure policies in ODRL and provides them to the Open Policy Agent(translated into rego). It can be used to + offer additional infromation to be taken into account. +* [did-helper](https://github.com/wistefan/did-helper) - a small helper application, providing the decentralized + identity to be used for the local Data Space + +After the deployment, the provider can create a policy to allow access to its data. An example policy can be found in +the [test-resources](../it/src/test/resources/policies/energyReport.json) +It allows every participant to access entities of type ```EnergyReport```. + +> :warning: The PAP and Scorpio APIs are only published to make demo interactions easier. +> In real environments, they should never be public without any authentication/authorization framework in front of them. + +The policy can be created at the PAP via: + +```shell + curl -s -X 'POST' http://pap-provider.127.0.0.1.nip.io:8080/policy \ + -H 'Content-Type: application/json' \ + -d '{ + "@context": { + "dc": "http://purl.org/dc/elements/1.1/", + "dct": "http://purl.org/dc/terms/", + "owl": "http://www.w3.org/2002/07/owl#", + "odrl": "http://www.w3.org/ns/odrl/2/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "skos": "http://www.w3.org/2004/02/skos/core#" + }, + "@id": "https://mp-operation.org/policy/common/type", + "@type": "odrl:Policy", + "odrl:permission": { + "odrl:assigner": { + "@id": "https://www.mp-operation.org/" + }, + "odrl:target": { + "@type": "odrl:AssetCollection", + "odrl:source": "urn:asset", + "odrl:refinement": [ + { + "@type": "odrl:Constraint", + "odrl:leftOperand": "ngsi-ld:entityType", + "odrl:operator": { + "@id": "odrl:eq" + }, + "odrl:rightOperand": "EnergyReport" + } + ] + }, + "odrl:assignee": { + "@id": "odrl:any" + }, + "odrl:action": { + "@id": "odrl:read" + } + } + }' +``` + +Data can be created through the NGSI-LD API itself. In order to make interaction easier, its directly available through +an ingress at ```http://scorpio-provider.127.0.0.1.nip.io/ngsi-ld/v1```. In +real environments, no endpoint should be publicly available without being protected by the authorization framework. +Create an entity via: + +```shell + curl -s -X POST http://scorpio-provider.127.0.0.1.nip.io:8080/ngsi-ld/v1/entities \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": "urn:ngsi-ld:EnergyReport:fms-1", + "type": "EnergyReport", + "name": { + "type": "Property", + "value": "Standard Server" + }, + "consumption": { + "type": "Property", + "value": "94" + } + }' +``` + +## Demo Interactions + +Once everything is deployed and configured(e.g. the consumer received a credential - +see [The Data Consumer](#the-data-consumer) - and policy/entity are setup - see [The Data Provider](#the-data-provider)) +, +the consumer can access the data as following: + +### Authenticate via OID4VP + +> :warning: Those steps assume that interaction with consumer and provider already happend, e.g. a VerifiableCredential +> is available +> and policy/entity are created. + +The credential needs to be presented for authentication +through [OID4VP]((https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). +Every required information for that flow can be retrieved via the standard endpoints. + +If you try to request the provider api without authentication, you will receive an 401: + +```shell + curl -s -X GET 'http://mp-data-service.127.0.0.1.nip.io:8080/ngsi-ld/v1/entities/urn:ngsi-ld:EnergyReport:fms-1' +``` + +The normal flow is now to request the oidc-information at the well-known endpoint: + +```shell + curl -s -X GET 'http://mp-data-service.127.0.0.1.nip.io:8080/.well-known/openid-configuration' +``` + +In the response, the grant type `vp_token` will be present, indicating the support for the OID4VP authentication flow: + +```json +{ + "issuer": "http://provider-verifier.127.0.0.1.nip.io:8080", + "authorization_endpoint": "http://provider-verifier.127.0.0.1.nip.io:8080", + "token_endpoint": "http://provider-verifier.127.0.0.1.nip.io:8080/token", + "jwks_uri": "http://provider-verifier.127.0.0.1.nip.io:8080/.well-known/jwks", + "scopes_supported": [ + "default" + ], + "response_types_supported": [ + "token" + ], + "response_mode_supported": [ + "direct_post" + ], + "grant_types_supported": [ + "authorization_code", + "vp_token" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "EdDSA", + "ES256" + ] +} +``` + +With that information, the authentication flow at the verifier(e.g.`"http://provider-verifier.127.0.0.1.nip.io:8080`) +can be started. +First, the credential needs to be encoded into a vp_token. If you want to do that manually, first a did and the +corresponding key-material is required. +You can create such via: + +```shell + docker run -v $(pwd):/cert quay.io/wi_stefan/did-helper:0.1.1 +``` + +This will produce the files cert.pem, cert.pfx, private-key.pem, public-key.pem and did.json, containing all required +information for the generated did:key. +Find the did here: + +```shell + export HOLDER_DID=$(cat did.json | jq '.id' -r); echo ${HOLDER_DID} +``` + +As a next step, a VerifiablePresentation, containing the Credential has to be created: + +```shell + export VERIFIABLE_PRESENTATION="{ + \"@context\": [\"https://www.w3.org/2018/credentials/v1\"], + \"type\": [\"VerifiablePresentation\"], + \"verifiableCredential\": [ + \"${VERIFIABLE_CREDENTIAL}\" + ], + \"holder\": \"${HOLDER_DID}\" + }"; echo ${VERIFIABLE_PRESENTATION} +``` + +Now, the presentation has to be embedded into a signed JWT: + +Setup the header: + +```shell + export JWT_HEADER=$(echo -n "{\"alg\":\"ES256\", \"typ\":\"JWT\", \"kid\":\"${HOLDER_DID}\"}"| base64 -w0 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//); echo Header: ${JWT_HEADER} +``` + +Setup the payload: + +```shell + export PAYLOAD=$(echo -n "{\"iss\": \"${HOLDER_DID}\", \"sub\": \"${HOLDER_DID}\", \"vp\": ${VERIFIABLE_PRESENTATION}}" | base64 -w0 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//); echo Payload: ${PAYLOAD}; +``` + +Create the signature: + +```shell + export SIGNATURE=$(echo -n "${JWT_HEADER}.${PAYLOAD}" | openssl dgst -sha256 -binary -sign private-key.pem | base64 -w0 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//); echo Signature: ${SIGNATURE}; +``` + +Combine them to the JWT: + +```shell + export JWT="${JWT_HEADER}.${PAYLOAD}.${SIGNATURE}"; echo The Token: ${JWT} +``` + +The JWT representation of the JWT has to be Base64-encoded(no padding!): + +```shell + export VP_TOKEN=$(echo -n ${JWT} | base64 -w0 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//); echo ${VP_TOKEN} +``` + +The vp_token can then be exchanged for the access-token + +```shell + export DATA_SERVICE_ACCESS_TOKEN=$(curl -s -X POST http://provider-verifier.127.0.0.1.nip.io:8080/token \ + --header 'Accept: */*' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --header 'client_id: data-service' \ + --data grant_type=vp_token \ + --data vp_token=${VP_TOKEN} \ + --data scope=default | jq '.access_token' -r ); echo ${DATA_SERVICE_ACCESS_TOKEN} +``` + +With that token, try to access the data again: + +```shell + curl -s -X GET 'http://mp-data-service.127.0.0.1.nip.io:8080/ngsi-ld/v1/entities/urn:ngsi-ld:EnergyReport:fms-1' \ + --header 'Accept: application/json' \ + --header "Authorization: Bearer ${DATA_SERVICE_ACCESS_TOKEN}" +``` + +## Deployment details + +In order to make the setup properly working locally and usable for development and try out, some adaptions have been +made and will be explained here. + +### Ingress + +To have the local environment as close to reality as possible, all interaction happens +through [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/). The ingress is provided via the +[Traefik-IngressController](https://doc.traefik.io/traefik/providers/kubernetes-ingress/) and configured +here: [k3s/infra/traefik]. Additionally, to ensure access to public endpoints happens +equally from inside the cluster and outside of it, [CoreDNS](https://coredns.io/)(deployed on default with k3s) is +instructed to resolve the ingresses(e.g. *.127.0.0.1.nip.io) directly to the loadbalancer-service of Traefik. + +Available ingresses: + +| URL | Component | Participant | Comment | Only for Demo | +|-------------------------------------------------|----------------------------------------------------------------------------|--------------|-------------------------------------------------------------------------------------|------------------------------------------------------| +| http://tir.127.0.0.1.nip.io:8080/ | [Trusted Issuers Registry](https://github.com/FIWARE/trusted-issuers-list) | Trust Anchor | Provides the list of trusted issuers | no | +| http://til.127.0.0.1.nip.io:8080/ | [Trusted Issuers List API](https://github.com/FIWARE/trusted-issuers-list) | Trust Anchor | Create,Update,Delete functionality for the Trusted Issuers Registry | should not be publicly available in real data spaces | +| http://keycloak-consumer.127.0.0.1.nip.io:8080/ | [Keycloak](https://github.com/keycloak/keycloak) | Consumer | Issues credentials on behalf of the consumer | no | +| http://did-consumer.127.0.0.1.nip.io:8080/ | [did-helper](https://github.com/wistefan/did-helper) | Consumer | Helper to provide access to the consumers did and key material | yes, should never be public | +| http://mp-data-service.127.0.0.1.nip.io:8080/ | [Apisix](https://apisix.apache.org/) | Provider | ApiGateway to be used as entry point to all secured services, f.e. the data-service | no | +| http://provider-verifier.127.0.0.1.nip.io:8080/ | [VCVerifier](https://github.com/FIWARE/VCVerifier) | Provider | Authentication endpoint, used for authenticating through VerifiableCredentials | no | +| http://did-provider.127.0.0.1.nip.io:8080/ | [did-helper](https://github.com/wistefan/did-helper) | Provider | Helper to provide access to the providers did and key material | yes, should never be public | +| http://scorpio-provider.127.0.0.1.nip.io:8080/ | [Scorpio ContextBroker](https://github.com/ScorpioBroker/ScorpioBroker) | Provider | Provides direct access to the context broker, to be used for test setup. | yes, should only be available through the PEP | +| http://pap-provider.127.0.0.1.nip.io:8080/ | [ODRL-PAP](https://github.com/wistefan/odrl-pap) | Provider | Allows configuration of the access policie, used for authorization | yes, should only be available to authorized users | + + +### Participant Identity + +In a Data Space, every participant requires an identity. The FIWARE Data Space relies on [Decentralized Identifiers](https://www.w3.org/TR/did-core/) +to identify its participants. While the concrete scheme to be used for a Data Space needs to be decided by its requirements, +the local installation uses [did:key](https://w3c-ccg.github.io/did-method-key/). While the did's are not well readable for humans, +they are well-supported, can be resolved without any external interaction and can easily be generated within the deployment. That makes +them a perfect fit for the local use-case. +All participants(e.g. the consumer and the participant) get a did generated on installation, by using the [did-helper](https://github.com/wistefan/did-helper). +The identities and connected key-material is automatically distributed in the cluster and set in the components that require it. + +In real world data spaces, the participants should rather use stabled identities, which can be [did:key](https://w3c-ccg.github.io/did-method-key/), but also +more organization-focused once like [did:web](https://w3c-ccg.github.io/did-method-web/) or [did:elsi](https://alastria.github.io/did-method-elsi/). + +### Deployment + +The deployment leverages the [k3s-maven-plugin](https://github.com/kokuwaio/k3s-maven-plugin) to stay as close to the real deployments as possible, +while providing integration with the [integration tests](../it/src/test). +In order to build a concrete deployment, [maven](https://maven.apache.org/) executes the following steps, that would also be done by a `normal` deployment process: + +1. Copy the required charts([charts/](../charts)) to the target folder, e.g. `target/charts` +2. Copy additionally required resources([k3s/infra & k3s/namespaces](../k3s/)) to the target folder, e.g. `target/k3s` +3. Execute `helm template` on the charts, with the local values provided for each participant(e.g. [trust-anchor](../k3s/trust-anchor.yaml), [provider](../k3s/provider.yaml) and [consumer](../k3s/consumer.yaml)) and copy the manifests to the target folder(e.g. `target/k3s`) +4. Spin up the cluster +5. Apply the infrastructure resources to the cluster, via `kubectl apply` +6. Apply the charts to the cluster, via `kubectl apply` \ No newline at end of file diff --git a/doc/img/components.drawio b/doc/img/components.drawio new file mode 100644 index 0000000..91ca379 --- /dev/null +++ b/doc/img/components.drawio @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/img/components.png b/doc/img/components.png new file mode 100644 index 0000000000000000000000000000000000000000..b7caf0a573c30550a02f365ab0b76f2dbffb1c31 GIT binary patch literal 124843 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�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%0Ay(SSwsQ>Ao$FXaC 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 n|5^2)pGsZbv?SsG*?U(Gq$qnUS + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/img/consumer.jpg b/doc/img/consumer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6fae3c7170c8c4a9f231a17d79ad0a316d123b51 GIT binary patch literal 14617 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 Mn*1M+gY1p}FBNE;4FCWD literal 0 HcmV?d00001 diff --git a/doc/img/overview.drawio b/doc/img/overview.drawio new file mode 100644 index 0000000..41638b6 --- /dev/null +++ b/doc/img/overview.drawio @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/img/overview.jpg b/doc/img/overview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f5e140dac46ddacf620707628eb0b8d6739b5ec7 GIT binary patch literal 13265 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 literal 0 HcmV?d00001 diff --git a/doc/img/provider.drawio b/doc/img/provider.drawio new file mode 100644 index 0000000..72172f2 --- /dev/null +++ b/doc/img/provider.drawio @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/img/provider.jpg b/doc/img/provider.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9872363b445bc985de69537813117f99e0c65c11 GIT binary patch literal 52416 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 $dscChartFolder/values.yaml -echo \ >> $dscChartFolder/values.yaml -echo '#Sub-Chart configuration configuration' >> $dscChartFolder/values.yaml -for dir in $dscChartFolder/charts/*/; do - chartsFile=${dir}Chart.yaml - valuesFile=${dir}values.yaml - name=$(yq e '.name' $chartsFile) - - echo \ >> $dscChartFolder/values.yaml - echo $name: >> $dscChartFolder/values.yaml - echo " # Enable the deployment of application: $name" >> $dscChartFolder/values.yaml - echo " deploymentEnabled: true" >> $dscChartFolder/values.yaml - echo \ >> $dscChartFolder/values.yaml - cat $valuesFile | sed 's/^/ /' >> $dscChartFolder/values.yaml - - helm dependency build ${dir} -done -# fix values in the chart yaml -version="${1:=0.0.0}" -yq e -i '.version = "'$version'"' $dscChartFolder/Chart.yaml - diff --git a/it/pom.xml b/it/pom.xml new file mode 100644 index 0000000..08e4b96 --- /dev/null +++ b/it/pom.xml @@ -0,0 +1,275 @@ + + + 4.0.0 + + org.fiware.dataspace + it + jar + + + connector + org.fiware.dataspace + 0.0.1 + + + + ${project.parent.basedir} + 1.18.32 + 2.0.6 + 2.0.1.Final + 2.14.2 + 1.3.2 + 4.2.0 + 5.9.2 + 7.11.1 + 24.0.4 + + + + + + + org.junit + junit-bom + ${version.org.junit.bom} + pom + import + + + io.cucumber + cucumber-bom + ${version.io.cucumber} + pom + import + + + + + + jitpack.io + https://jitpack.io + + + + + + org.projectlombok + lombok + ${version.org.projectlombok} + + + org.slf4j + slf4j-api + ${version.org.slf4j.slf4j-api} + + + javax.validation + validation-api + ${version.javax.validation} + + + com.fasterxml.jackson.core + jackson-annotations + ${version.com.fasterxml.jackson.core} + + + com.squareup.okhttp + okhttp + 2.7.5 + + + + + com.github.multiformats + java-multibase + v1.1.1 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.platform + junit-platform-suite + test + + + org.awaitility + awaitility + ${version.org.awaitility} + test + + + io.cucumber + cucumber-java + test + + + io.cucumber + cucumber-junit-platform-engine + test + + + org.keycloak + keycloak-admin-client + ${version.org.keycloak} + test + + + org.keycloak + keycloak-core + ${version.org.keycloak} + test + + + org.keycloak + keycloak-crypto-default + ${version.org.keycloak} + test + + + org.keycloak + keycloak-services + ${version.org.keycloak} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${jdk.version} + ${release.version} + + + org.projectlombok + lombok + ${version.org.projectlombok} + + + + + + + + + + local + + + + maven-resources-plugin + + true + + + + io.kokuwa.maven + helm-maven-plugin + + true + + + + io.kokuwa.maven + k3s-maven-plugin + + true + + + + + + + test + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.org.apache.maven.plugins.maven-jar} + + true + + + + maven-resources-plugin + + false + + + + copy-resources-trust-anchor + validate + + + copy-resources-namespaces + validate + + copy-resources + + + + copy-resources-dsc + validate + + copy-resources + + + + + + io.kokuwa.maven + helm-maven-plugin + + false + + + + template-dsc-consumer + test-compile + + + template-dsc-provider + test-compile + + + template-trust-anchor + test-compile + + + + + io.kokuwa.maven + k3s-maven-plugin + + false + + + + apply-participants + pre-integration-test + + + create-namespaces + pre-integration-test + + + + + + + + \ No newline at end of file diff --git a/it/src/test/java/org/fiware/dataspace/it/components/FancyMarketplaceEnvironment.java b/it/src/test/java/org/fiware/dataspace/it/components/FancyMarketplaceEnvironment.java new file mode 100644 index 0000000..e455f85 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/FancyMarketplaceEnvironment.java @@ -0,0 +1,22 @@ +package org.fiware.dataspace.it.components; + +/** + * @author Stefan Wiedemann + */ +public abstract class FancyMarketplaceEnvironment { + public static final String DID_CONSUMER_ADDRESS = "http://did-consumer.127.0.0.1.nip.io:8080"; + public static final String CONSUMER_KEYCLOAK_ADDRESS = "http://keycloak-consumer.127.0.0.1.nip.io:8080"; + + public static final String OIDC_WELL_KNOWN_PATH = "/.well-known/openid-configuration"; + private static final String TEST_REALM = "test-realm"; + private static final String TEST_USER_NAME = "test-user"; + private static final String TEST_USER_PASSWORD = "test"; + + /** + * Returns an access token to be used with Keycloak. + */ + public static String loginToConsumerKeycloak() { + KeycloakHelper consumerKeycloak = new KeycloakHelper(TEST_REALM, CONSUMER_KEYCLOAK_ADDRESS); + return consumerKeycloak.getUserToken(TEST_USER_NAME, TEST_USER_PASSWORD); + } +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/KeycloakHelper.java b/it/src/test/java/org/fiware/dataspace/it/components/KeycloakHelper.java new file mode 100644 index 0000000..66b903a --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/KeycloakHelper.java @@ -0,0 +1,32 @@ +package org.fiware.dataspace.it.components; + +import lombok.RequiredArgsConstructor; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.admin.client.token.TokenManager; +import org.keycloak.representations.idm.ClientRepresentation; + +import java.util.List; + +/** + * @author Stefan Wiedemann + */ +@RequiredArgsConstructor +public class KeycloakHelper { + private final String realm; + private final String address; + + public String getUserToken(String username, String password) { + + TokenManager tokenManager = KeycloakBuilder.builder() + .username(username) + .password(password) + .realm(realm) + .grantType("password") + .clientId("admin-cli") + .serverUrl(address) + .build() + .tokenManager(); + return tokenManager.getAccessToken().getToken(); + } +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/MPOperationsEnvironment.java b/it/src/test/java/org/fiware/dataspace/it/components/MPOperationsEnvironment.java new file mode 100644 index 0000000..6eb6d13 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/MPOperationsEnvironment.java @@ -0,0 +1,37 @@ +package org.fiware.dataspace.it.components; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import org.apache.http.HttpStatus; +import org.fiware.dataspace.it.components.model.OpenIdConfiguration; + +import static org.fiware.dataspace.it.components.TestUtils.OBJECT_MAPPER; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Stefan Wiedemann + */ +public abstract class MPOperationsEnvironment { + + public static final String DID_PROVIDER_ADDRESS = "http://did-provider.127.0.0.1.nip.io:8080"; + public static final String PROVIDER_PAP_ADDRESS = "http://pap-provider.127.0.0.1.nip.io:8080"; + public static final String PROVIDER_API_ADDRESS = "http://mp-data-service.127.0.0.1.nip.io:8080"; + public static final String SCORPIO_ADDRESS = "http://scorpio-provider.127.0.0.1.nip.io:8080"; + + public static final String OIDC_WELL_KNOWN_PATH = "/.well-known/openid-configuration"; + public static final String CLIENT_ID = "data-service"; + private static final OkHttpClient HTTP_CLIENT = new OkHttpClient(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public static OpenIdConfiguration getOpenIDConfiguration() throws Exception { + Request wellKnownRequest = new Request.Builder().get() + .url(PROVIDER_API_ADDRESS + OIDC_WELL_KNOWN_PATH) + .build(); + Response wellKnownResponse = HTTP_CLIENT.newCall(wellKnownRequest).execute(); + assertEquals(HttpStatus.SC_OK, wellKnownResponse.code(), "The oidc config should have been returned."); + return OBJECT_MAPPER.readValue(wellKnownResponse.body().string(), OpenIdConfiguration.class); + } + +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/RunCucumberTest.java b/it/src/test/java/org/fiware/dataspace/it/components/RunCucumberTest.java new file mode 100644 index 0000000..ff2c2a2 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/RunCucumberTest.java @@ -0,0 +1,18 @@ +package org.fiware.dataspace.it.components; + +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; + +import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; + +/** + * @author Stefan Wiedemann + */ +@Suite +@IncludeEngines("cucumber") +@SelectClasspathResource("it") +@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") +public class RunCucumberTest { +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/StepDefinitions.java b/it/src/test/java/org/fiware/dataspace/it/components/StepDefinitions.java new file mode 100644 index 0000000..650f456 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/StepDefinitions.java @@ -0,0 +1,181 @@ +package org.fiware.dataspace.it.components; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import io.cucumber.java.After; +import io.cucumber.java.Before; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.fiware.dataspace.it.components.model.OpenIdConfiguration; +import org.keycloak.common.crypto.CryptoIntegration; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Security; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Stefan Wiedemann + */ +@Slf4j +public class StepDefinitions { + + private static final OkHttpClient HTTP_CLIENT = new OkHttpClient(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String USER_CREDENTIAL = "user-credential"; + private static final String GRANT_TYPE_VP_TOKEN = "vp_token"; + private static final String RESPONSE_TYPE_DIRECT_POST = "direct_post"; + + private Wallet fancyMarketplaceEmployeeWallet; + + private List createdPolicies = new ArrayList<>(); + private List createdEntities = new ArrayList<>(); + + @Before + public void setup() throws Exception { + CryptoIntegration.init(this.getClass().getClassLoader()); + Security.addProvider(new BouncyCastleProvider()); + fancyMarketplaceEmployeeWallet = new Wallet(); + } + + @After + public void cleanUp() { + cleanUpPolicies(); + cleanUpEntities(); + } + + private void cleanUpPolicies() { + createdPolicies.forEach(policyId -> { + Request deletionRequest = new Request.Builder() + .url(MPOperationsEnvironment.PROVIDER_PAP_ADDRESS + "/policy/" + policyId) + .delete() + .build(); + try { + HTTP_CLIENT.newCall(deletionRequest).execute(); + } catch (IOException e) { + // just log + log.warn("Was not able to clean up policy {}.", policyId); + } + }); + } + + private void cleanUpEntities() { + createdEntities.forEach(entityId -> { + Request deletionRequest = new Request.Builder() + .url(MPOperationsEnvironment.SCORPIO_ADDRESS + "/ngsi-ld/v1/entities/" + entityId) + .delete() + .build(); + try { + HTTP_CLIENT.newCall(deletionRequest).execute(); + } catch (IOException e) { + // just log + log.warn("Was not able to clean up entitiy {}.", entityId); + } + }); + } + + @Given("M&P Operations is registered as a participant in the data space.") + public void checkMPRegistered() throws Exception { + Request didCheckRequest = new Request.Builder() + .url(TrustAnchorEnvironment.TIR_ADDRESS + "/v4/issuers/" + getDid(MPOperationsEnvironment.DID_PROVIDER_ADDRESS)) + .build(); + assertEquals(HttpStatus.SC_OK, HTTP_CLIENT.newCall(didCheckRequest).execute().code(), "The did should be registered at the trust-anchor."); + } + + + @Given("Fancy Marketplace is registered as a participant in the data space.") + public void checkFMRegistered() throws Exception { + Request didCheckRequest = new Request.Builder() + .url(TrustAnchorEnvironment.TIR_ADDRESS + "/v4/issuers/" + getDid(FancyMarketplaceEnvironment.DID_CONSUMER_ADDRESS)) + .build(); + assertEquals(HttpStatus.SC_OK, HTTP_CLIENT.newCall(didCheckRequest).execute().code(), "The did should be registered at the trust-anchor."); + } + + @When("M&P Operations registers a policy to allow every participant access to its energy reports.") + public void mpRegisterEnergyReportPolicy() throws Exception { + RequestBody policyBody = RequestBody.create(MediaType.parse("application/json"), getPolicy("energyReport")); + Request policyCreationRequest = new Request.Builder() + .post(policyBody) + .url(MPOperationsEnvironment.PROVIDER_PAP_ADDRESS + "/policy") + .build(); + Response policyCreationResponse = HTTP_CLIENT.newCall(policyCreationRequest).execute(); + assertEquals(HttpStatus.SC_OK, policyCreationResponse.code(), "The policy should have been created."); + createdPolicies.add(policyCreationResponse.header("Location")); + } + + @When("M&P Operations creates an energy report.") + public void createEnergyReport() throws Exception { + Map offerEntity = Map.of("type", "EnergyReport", + "id", "urn:ngsi-ld:EnergyReport:fms-1", + "name", Map.of("type", "Property", "value", "Standard Server"), + "consumption", Map.of("type", "Property", "value", "94")); + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), OBJECT_MAPPER.writeValueAsString(offerEntity)); + + Request creationRequest = new Request.Builder() + .url(MPOperationsEnvironment.SCORPIO_ADDRESS + "/ngsi-ld/v1/entities") + .post(requestBody) + .build(); + assertEquals(HttpStatus.SC_CREATED, HTTP_CLIENT.newCall(creationRequest).execute().code(), "The entity should have been created."); + createdEntities.add("urn:ngsi-ld:EnergyReport:fms-1"); + } + + @When("Fancy Marketplace issues a credential to its employee.") + public void issueCredentialToEmployee() throws Exception { + String accessToken = FancyMarketplaceEnvironment.loginToConsumerKeycloak(); + fancyMarketplaceEmployeeWallet.getCredentialFromIssuer(accessToken, FancyMarketplaceEnvironment.CONSUMER_KEYCLOAK_ADDRESS, USER_CREDENTIAL); + } + + @Then("Fancy Marketplace' employee can access the EnergyReport.") + public void accessTheEnergyReport() throws Exception { + OpenIdConfiguration openIdConfiguration = MPOperationsEnvironment.getOpenIDConfiguration(); + assertTrue(openIdConfiguration.getGrantTypesSupported().contains(GRANT_TYPE_VP_TOKEN), "The M&P environment should support vp_tokens"); + assertTrue(openIdConfiguration.getResponseModeSupported().contains(RESPONSE_TYPE_DIRECT_POST), "The M&P environment should support direct_post"); + assertNotNull(openIdConfiguration.getTokenEndpoint(), "The M&P environment should provide a token endpoint."); + + String accessToken = fancyMarketplaceEmployeeWallet.exchangeCredentialForToken(openIdConfiguration, USER_CREDENTIAL); + Request authenticatedEntityRequest = new Request.Builder().get() + .url(MPOperationsEnvironment.PROVIDER_API_ADDRESS + "/ngsi-ld/v1/entities/urn:ngsi-ld:EnergyReport:fms-1") + .addHeader("Authorization", "Bearer " + accessToken) + .addHeader("Accept", "application/json") + .build(); + + Awaitility.await() + .atMost(Duration.ofSeconds(60)) + .until(() -> { + return HttpStatus.SC_OK == HTTP_CLIENT.newCall(authenticatedEntityRequest).execute().code(); + }); + } + + private String getDid(String didHelperAddress) throws Exception { + Request didRequest = new Request.Builder().url(didHelperAddress + "/did-material/did.env").build(); + Response didResponse = HTTP_CLIENT.newCall(didRequest).execute(); + String didEnv = didResponse.body().string(); + return didEnv.split("=")[1]; + } + + private String getPolicy(String policyName) throws IOException { + InputStream policyInputStream = this.getClass().getResourceAsStream(String.format("/policies/%s.json", policyName)); + StringBuilder sb = new StringBuilder(); + for (int ch; (ch = policyInputStream.read()) != -1; ) { + sb.append((char) ch); + } + return sb.toString(); + } + +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/TestUtils.java b/it/src/test/java/org/fiware/dataspace/it/components/TestUtils.java new file mode 100644 index 0000000..4b76c5b --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/TestUtils.java @@ -0,0 +1,127 @@ +package org.fiware.dataspace.it.components; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.cucumber.java.sl.In; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.checkerframework.checker.units.qual.C; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.common.util.PemUtils; +import org.keycloak.crypto.KeyWrapper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Map; + +/** + * Helper methods for the test. + */ +public class TestUtils { + + private static final String CONSUMER_DID_HELPER = "http://did-consumer.127.0.0.1.nip.io:8080"; + private static final String PROVIDER_DID_HELPER = "http://did-provider.127.0.0.1.nip.io:8080"; + private static final String PRIVATE_KEY_PATH = "/did-material/private-key.pem"; + private static final String DID_ENV_PATH = "/did-material/did.env"; + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + private static final HttpClient HTTP_CLIENT = HttpClient + .newBuilder() + // we don´t follow the redirect directly, since we are not a real wallet + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + private TestUtils() { + // prevent instantiation + } + + public static String getFormDataAsString(Map formData) { + StringBuilder formBodyBuilder = new StringBuilder(); + for (Map.Entry singleEntry : formData.entrySet()) { + if (formBodyBuilder.length() > 0) { + formBodyBuilder.append("&"); + } + formBodyBuilder.append(URLEncoder.encode(singleEntry.getKey(), StandardCharsets.UTF_8)); + formBodyBuilder.append("="); + formBodyBuilder.append(URLEncoder.encode(singleEntry.getValue(), StandardCharsets.UTF_8)); + } + return formBodyBuilder.toString(); + } + + public static String getConsumerDid() throws Exception { + return getDid(CONSUMER_DID_HELPER); + } + + public static String getProviderDid() throws Exception { + return getDid(PROVIDER_DID_HELPER); + } + + private static String getDid(String host) throws Exception { + HttpRequest didRequest = HttpRequest.newBuilder() + .uri(URI.create(host + DID_ENV_PATH)) + .GET() + .build(); + HttpResponse didResponse = HTTP_CLIENT.send(didRequest, HttpResponse.BodyHandlers.ofInputStream()); + return getDidFromEnv(didResponse.body()); + } + + private static String getDidFromEnv(InputStream didInputStream) throws Exception { + String didEnvString = new String(didInputStream.readAllBytes(), StandardCharsets.UTF_8); + return didEnvString.split("=")[1]; + } + + private static KeyWrapper getKey(String host) throws Exception { + HttpRequest keyRequest = HttpRequest.newBuilder() + .uri(URI.create(host + PRIVATE_KEY_PATH)) + .GET() + .build(); + HttpResponse keyResponse = HTTP_CLIENT.send(keyRequest, HttpResponse.BodyHandlers.ofInputStream()); + return getKeyWrapper(keyResponse.body()); + } + + public static KeyWrapper getConsumerKey() throws Exception { + return getKey(CONSUMER_DID_HELPER); + } + + public static KeyWrapper getProviderKey() throws Exception { + return getKey(PROVIDER_DID_HELPER); + } + + private static KeyWrapper getKeyWrapper(InputStream keyInputStream) throws Exception { + String keyString = new String(keyInputStream.readAllBytes(), StandardCharsets.UTF_8); + PEMParser pemParser = new PEMParser(new StringReader(keyString)); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + Object object = pemParser.readObject(); + KeyPair kp = converter.getKeyPair((PEMKeyPair) object); + PrivateKey privateKey = kp.getPrivate(); + KeyWrapper keyWrapper = new KeyWrapper(); + keyWrapper.setPrivateKey(privateKey); + return keyWrapper; + } +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/TrustAnchorEnvironment.java b/it/src/test/java/org/fiware/dataspace/it/components/TrustAnchorEnvironment.java new file mode 100644 index 0000000..2dc80f9 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/TrustAnchorEnvironment.java @@ -0,0 +1,9 @@ +package org.fiware.dataspace.it.components; + +/** + * @author Stefan Wiedemann + */ +public abstract class TrustAnchorEnvironment { + + public static final String TIR_ADDRESS = "http://tir.127.0.0.1.nip.io:8080"; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/Wallet.java b/it/src/test/java/org/fiware/dataspace/it/components/Wallet.java new file mode 100644 index 0000000..e7243e9 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/Wallet.java @@ -0,0 +1,298 @@ +package org.fiware.dataspace.it.components; + +import com.squareup.okhttp.FormEncodingBuilder; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import io.ipfs.multibase.Multibase; +import jakarta.ws.rs.core.MediaType; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; +import org.bouncycastle.jce.interfaces.ECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.fiware.dataspace.it.components.model.Credential; +import org.fiware.dataspace.it.components.model.CredentialOffer; +import org.fiware.dataspace.it.components.model.CredentialRequest; +import org.fiware.dataspace.it.components.model.Grant; +import org.fiware.dataspace.it.components.model.IssuerConfiguration; +import org.fiware.dataspace.it.components.model.OfferUri; +import org.fiware.dataspace.it.components.model.OpenIdConfiguration; +import org.fiware.dataspace.it.components.model.SupportedConfiguration; +import org.fiware.dataspace.it.components.model.TokenResponse; +import org.fiware.dataspace.it.components.model.VerifiablePresentation; +import org.keycloak.crypto.ECDSASignatureSignerContext; +import org.keycloak.crypto.KeyUse; +import org.keycloak.crypto.KeyWrapper; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.representations.JsonWebToken; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.spec.ECGenParameterSpec; +import java.time.Clock; +import java.util.Base64; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.fiware.dataspace.it.components.TestUtils.OBJECT_MAPPER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Stefan Wiedemann + */ +@Slf4j +public class Wallet { + + private static final String OPENID_CREDENTIAL_ISSUER_PATH = "/realms/test-realm/.well-known/openid-credential-issuer"; + private static final String CREDENTIAL_OFFER_URI_PATH = "/realms/test-realm/protocol/oid4vc/credential-offer-uri"; + private static final String OID_WELL_KNOWN_PATH = "/.well-known/openid-configuration"; + private static final String PRE_AUTHORIZED_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code"; + + private static final String SAME_DEVICE_ENDPOINT = "/api/v1/samedevice"; + + private final Map credentialStorage = new HashMap<>(); + + private static final OkHttpClient HTTP_CLIENT = new OkHttpClient(); + + private final KeyWrapper walletKey; + private final String did; + + public Wallet() throws Exception { + walletKey = getECKey(); + did = walletKey.getKid(); + } + + + public String exchangeCredentialForToken(OpenIdConfiguration openIdConfiguration, String credentialId) throws Exception { + String vpToken = Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(createVPToken(did, walletKey, credentialStorage.get(credentialId)).getBytes()); + RequestBody requestBody = new FormEncodingBuilder() + .add("grant_type", "vp_token") + .add("vp_token", vpToken) + .add("scope", "default") + .build(); + Request tokenRequest = new Request.Builder() + .post(requestBody) + .addHeader("client_id", MPOperationsEnvironment.CLIENT_ID) + .url(openIdConfiguration.getTokenEndpoint()) + .build(); + Response tokenResponse = HTTP_CLIENT.newCall(tokenRequest).execute(); + + assertEquals(HttpStatus.SC_OK, tokenResponse.code(), "A token should have been responded."); + + TokenResponse accessTokenResponse = OBJECT_MAPPER.readValue(tokenResponse.body().string(), TokenResponse.class); + assertNotNull(accessTokenResponse.getAccessToken(), "The access token should have been returned."); + return accessTokenResponse.getAccessToken(); + } + + private String createVPToken(String did, KeyWrapper key, String credential) { + VerifiablePresentation verifiablePresentation = new VerifiablePresentation(); + verifiablePresentation.setVerifiableCredential(List.of(credential)); + verifiablePresentation.setHolder(did); + key.setKid(did); + key.setAlgorithm("ES256"); + key.setUse(KeyUse.SIG); + + ECDSASignatureSignerContext signerContext = new ECDSASignatureSignerContext(key); + + JsonWebToken jwt = new JsonWebToken() + .issuer(did) + .subject(did) + .iat(Clock.systemUTC().millis()); + jwt.setOtherClaims("vp", verifiablePresentation); + return new JWSBuilder() + .type("JWT") + .jsonContent(jwt) + .sign(signerContext); + } + + + public void getCredentialFromIssuer(String userToken, String issuerHost, String credentialId) throws Exception { + IssuerConfiguration issuerConfiguration = getIssuerConfiguration(issuerHost); + OfferUri offerUri = getCredentialOfferUri(userToken, issuerHost, credentialId); + CredentialOffer credentialOffer = getCredentialOffer(userToken, offerUri); + + var theCredential = getCredential(issuerConfiguration, credentialOffer); + credentialStorage.put(credentialId, theCredential); + } + + + public IssuerConfiguration getIssuerConfiguration(String issuerHost) throws Exception { + + Request configRequest = new Request.Builder() + .get() + .url(issuerHost + OPENID_CREDENTIAL_ISSUER_PATH) + .build(); + Response configResponse = HTTP_CLIENT.newCall(configRequest).execute(); + + assertEquals(HttpStatus.SC_OK, configResponse.code(), "An issuer config should have been returned."); + return OBJECT_MAPPER.readValue(configResponse.body().string(), IssuerConfiguration.class); + } + + public OfferUri getCredentialOfferUri(String keycloakJwt, String issuerHost, String credentialConfigId) throws Exception { + + Request request = new Request.Builder() + .url(issuerHost + CREDENTIAL_OFFER_URI_PATH + "?credential_configuration_id=" + credentialConfigId) + .get() + .header("Authorization", "Bearer " + keycloakJwt) + .build(); + + Response uriResponse = HTTP_CLIENT.newCall(request).execute(); + + assertEquals(HttpStatus.SC_OK, uriResponse.code(), "An uri should have been returned."); + return OBJECT_MAPPER.readValue(uriResponse.body().string(), OfferUri.class); + } + + public CredentialOffer getCredentialOffer(String keycloakJwt, OfferUri offerUri) throws Exception { + + Request uriRequest = new Request.Builder() + .get() + .url(offerUri.getIssuer() + offerUri.getNonce()) + .header("Authorization", "Bearer " + keycloakJwt) + .build(); + + Response offerResponse = HTTP_CLIENT.newCall(uriRequest).execute(); + + assertEquals(HttpStatus.SC_OK, offerResponse.code(), "An offer should have been returned."); + return OBJECT_MAPPER.readValue(offerResponse.body().string(), CredentialOffer.class); + } + + public String getTokenForOffer(IssuerConfiguration issuerConfiguration, CredentialOffer credentialOffer) throws Exception { + String authorizationServer = issuerConfiguration.getAuthorizationServers().get(0); + OpenIdConfiguration openIdConfiguration = getOpenIdConfiguration(authorizationServer); + assertTrue(openIdConfiguration.getGrantTypesSupported().contains(PRE_AUTHORIZED_GRANT_TYPE), "The grant type should actually be supported by the authorization server."); + + Grant preAuthorizedGrant = credentialOffer.getGrants().get(PRE_AUTHORIZED_GRANT_TYPE); + return getAccessToken(openIdConfiguration.getTokenEndpoint(), preAuthorizedGrant.getPreAuthorizedCode()); + } + + public String getCredential(IssuerConfiguration issuerConfiguration, CredentialOffer credentialOffer) throws Exception { + String accessToken = getTokenForOffer(issuerConfiguration, credentialOffer); + + String credentialResponse = credentialOffer.getCredentialConfigurationIds() + .stream() + .map(offeredCredentialId -> issuerConfiguration.getCredentialConfigurationsSupported().get(offeredCredentialId)) + .map(supportedCredential -> { + try { + return requestOffer(accessToken, issuerConfiguration.getCredentialEndpoint(), supportedCredential); + } catch (Exception e) { + return null; + } + }) + .filter(Objects::nonNull) + .findFirst() + .get(); + return OBJECT_MAPPER.readValue(credentialResponse, Credential.class).getCredential(); + } + + private String requestOffer(String token, String credentialEndpoint, SupportedConfiguration offeredCredential) throws Exception { + CredentialRequest credentialRequest = new CredentialRequest(); + credentialRequest.setCredentialIdentifier(offeredCredential.getId()); + credentialRequest.setFormat(offeredCredential.getFormat()); + + RequestBody credentialRequestBody = RequestBody + .create( + com.squareup.okhttp.MediaType.parse(MediaType.APPLICATION_JSON), + OBJECT_MAPPER.writeValueAsString(credentialRequest)); + Request credentialHttpRequest = new Request.Builder() + .post(credentialRequestBody) + .url(credentialEndpoint) + .header("Authorization", "Bearer " + token) + .header("Content-Type", MediaType.APPLICATION_JSON) + .build(); + + + Response credentialResponse = HTTP_CLIENT.newCall(credentialHttpRequest).execute(); + assertEquals(HttpStatus.SC_OK, credentialResponse.code(), "A credential should have been returned."); + return credentialResponse.body().string(); + } + + public String getAccessToken(String tokenEndpoint, String preAuthorizedCode) throws Exception { + RequestBody requestBody = new FormEncodingBuilder() + .add("grant_type", PRE_AUTHORIZED_GRANT_TYPE) + .add("code", preAuthorizedCode) + .build(); + Request tokenRequest = new Request.Builder() + .url(tokenEndpoint) + .post(requestBody) + .build(); + + + Response tokenResponse = HTTP_CLIENT.newCall(tokenRequest).execute(); + assertEquals(HttpStatus.SC_OK, tokenResponse.code(), "A valid token should have been returned."); + + return OBJECT_MAPPER.readValue(tokenResponse.body().string(), TokenResponse.class).getAccessToken(); + } + + public OpenIdConfiguration getOpenIdConfiguration(String authorizationServer) throws Exception { + Request request = new Request.Builder() + .get() + .url(authorizationServer + OID_WELL_KNOWN_PATH) + .build(); + Response openIdConfigResponse = HTTP_CLIENT.newCall(request).execute(); + + assertEquals(HttpStatus.SC_OK, openIdConfigResponse.code(), "An openId config should have been returned."); + return OBJECT_MAPPER.readValue(openIdConfigResponse.body().string(), OpenIdConfiguration.class); + } + + + private static KeyWrapper getECKey() throws Exception { + try { + Security.addProvider(new BouncyCastleProvider()); + + KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", "BC"); + ECGenParameterSpec spec = new ECGenParameterSpec("P-256"); + kpGen.initialize(spec); + + var keyPair = kpGen.generateKeyPair(); + KeyWrapper kw = new KeyWrapper(); + kw.setPrivateKey(keyPair.getPrivate()); + kw.setPublicKey(keyPair.getPublic()); + kw.setUse(KeyUse.SIG); + kw.setKid(generateDid(keyPair)); + kw.setType("EC"); + kw.setAlgorithm("ES256"); + return kw; + + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } + + private static String generateDid(KeyPair keyPair) throws Exception { + if (keyPair.getPublic() instanceof ECPublicKey ecPublicKey) { + byte[] encodedQ = ecPublicKey.getQ().getEncoded(true); + byte[] codecBytes = new byte[2]; + codecBytes[0] = HexFormat.of().parseHex("80")[0]; + codecBytes[1] = HexFormat.of().parseHex("24")[0]; + + byte[] prefixed = new byte[encodedQ.length + codecBytes.length]; + System.arraycopy(codecBytes, 0, prefixed, 0, codecBytes.length); + System.arraycopy(encodedQ, 0, prefixed, codecBytes.length, encodedQ.length); + + String encodedKeyRaw = Multibase.encode(Multibase.Base.Base58BTC, prefixed); + return "did:key:" + encodedKeyRaw; + } + throw new IllegalArgumentException("Key pair is not supported."); + } + + private static byte[] marshalCompressed(BigInteger x, BigInteger y) { + // we only support the P-256 curve here, thus a fixed length can be set + byte[] compressed = new byte[33]; + compressed[0] = (byte) (y.testBit(0) ? 1 : 0); + System.arraycopy(x.toByteArray(), 0, compressed, 1, x.toByteArray().length); + return compressed; + } + +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/Credential.java b/it/src/test/java/org/fiware/dataspace/it/components/model/Credential.java new file mode 100644 index 0000000..e2b1378 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/Credential.java @@ -0,0 +1,18 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Credential { + + private String credential; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/CredentialOffer.java b/it/src/test/java/org/fiware/dataspace/it/components/model/CredentialOffer.java new file mode 100644 index 0000000..0127aa3 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/CredentialOffer.java @@ -0,0 +1,29 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CredentialOffer { + + @JsonProperty("grants") + private Map grants; + + @JsonProperty("credential_issuer") + private String credentialIssuer; + + @JsonProperty("credential_configuration_ids") + private List credentialConfigurationIds; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/CredentialRequest.java b/it/src/test/java/org/fiware/dataspace/it/components/model/CredentialRequest.java new file mode 100644 index 0000000..15afd4a --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/CredentialRequest.java @@ -0,0 +1,22 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CredentialRequest { + + private Format format; + + @JsonProperty("credential_identifier") + private String credentialIdentifier; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/Format.java b/it/src/test/java/org/fiware/dataspace/it/components/model/Format.java new file mode 100644 index 0000000..e947716 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/Format.java @@ -0,0 +1,81 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://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 governing permissions and + * limitations under the License. + */ + +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Enum of supported credential formats + * + * @author Stefan Wiedemann + */ +public enum Format { + + /** + * LD-Credentials {@see https://www.w3.org/TR/vc-data-model/} + */ + LDP_VC("ldp_vc"), + + /** + * JWT-Credentials {@see https://identity.foundation/jwt-vc-presentation-profile/} + */ + JWT_VC("jwt_vc"), + + /** + * SD-JWT-Credentials {@see https://drafts.oauth.net/oauth-sd-jwt-vc/draft-ietf-oauth-sd-jwt-vc.html} + */ + SD_JWT_VC("sd-jwt_vc"); + + private String value; + + Format(String value) { + this.value = value; + } + + /** + * Convert a String into String, as specified in the + * See JAX RS 2.0 Specification, section 3.2, p. 12 + */ + public static Format fromString(String s) { + for (Format b : Format.values()) { + // using Objects.toString() to be safe if value type non-object type + // because types like 'int' etc. will be auto-boxed + if (java.util.Objects.toString(b.value).equals(s)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected string value '" + s + "'"); + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static Format fromValue(String value) { + for (Format b : Format.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} \ No newline at end of file diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/Grant.java b/it/src/test/java/org/fiware/dataspace/it/components/model/Grant.java new file mode 100644 index 0000000..4052d3b --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/Grant.java @@ -0,0 +1,20 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Grant { + + @JsonProperty("pre-authorized_code") + private String preAuthorizedCode; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/IssuerConfiguration.java b/it/src/test/java/org/fiware/dataspace/it/components/model/IssuerConfiguration.java new file mode 100644 index 0000000..c348b97 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/IssuerConfiguration.java @@ -0,0 +1,32 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class IssuerConfiguration { + + @JsonProperty("credential_endpoint") + private String credentialEndpoint; + + @JsonProperty("credential_issuer") + private String credentialIssuer; + + @JsonProperty("authorization_servers") + private List authorizationServers; + + @JsonProperty("credential_configurations_supported") + private Map credentialConfigurationsSupported; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/OfferUri.java b/it/src/test/java/org/fiware/dataspace/it/components/model/OfferUri.java new file mode 100644 index 0000000..887d522 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/OfferUri.java @@ -0,0 +1,19 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class OfferUri { + + private String issuer; + private String nonce; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/OpenIdConfiguration.java b/it/src/test/java/org/fiware/dataspace/it/components/model/OpenIdConfiguration.java new file mode 100644 index 0000000..e21b330 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/OpenIdConfiguration.java @@ -0,0 +1,40 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * OID Configuration, that maps only those fields we need for the pre-authorized flow + * + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class OpenIdConfiguration { + + @JsonProperty("token_endpoint") + private String tokenEndpoint; + + @JsonProperty("authorization_endpoint") + private String authorizationEndpoint; + + @JsonProperty("jwks_uri") + private String jwksUri; + + @JsonProperty("scopes_supported") + private List scopesSupported; + + @JsonProperty("response_mode_supported") + private List responseModeSupported; + + @JsonProperty("grant_types_supported") + private List grantTypesSupported; + +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/SupportedConfiguration.java b/it/src/test/java/org/fiware/dataspace/it/components/model/SupportedConfiguration.java new file mode 100644 index 0000000..910699e --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/SupportedConfiguration.java @@ -0,0 +1,26 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SupportedConfiguration { + + @JsonProperty("id") + private String id; + + @JsonProperty("format") + private Format format; + + @JsonProperty("scope") + private String scope; +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/TokenResponse.java b/it/src/test/java/org/fiware/dataspace/it/components/model/TokenResponse.java new file mode 100644 index 0000000..dd2f249 --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/TokenResponse.java @@ -0,0 +1,25 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class TokenResponse { + + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("token_type") + private String tokenType; + @JsonProperty("expires_in") + private long expiresIn; + +} diff --git a/it/src/test/java/org/fiware/dataspace/it/components/model/VerifiablePresentation.java b/it/src/test/java/org/fiware/dataspace/it/components/model/VerifiablePresentation.java new file mode 100644 index 0000000..5912eed --- /dev/null +++ b/it/src/test/java/org/fiware/dataspace/it/components/model/VerifiablePresentation.java @@ -0,0 +1,26 @@ +package org.fiware.dataspace.it.components.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @author Stefan Wiedemann + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class VerifiablePresentation { + + private List type = List.of("VerifiablePresentation"); + private List verifiableCredential; + private String holder; + @JsonProperty("@context") + private List context = List.of("https://www.w3.org/2018/credentials/v1"); + +} diff --git a/it/src/test/resources/it/mvds_basic.feature b/it/src/test/resources/it/mvds_basic.feature new file mode 100644 index 0000000..84825d1 --- /dev/null +++ b/it/src/test/resources/it/mvds_basic.feature @@ -0,0 +1,9 @@ +Feature: The Data Space should support a basic data exchange between registered participants. + + Scenario: A registered consumer can retrieve data from a registered data provider. + Given M&P Operations is registered as a participant in the data space. + And Fancy Marketplace is registered as a participant in the data space. + When M&P Operations registers a policy to allow every participant access to its energy reports. + And M&P Operations creates an energy report. + And Fancy Marketplace issues a credential to its employee. + Then Fancy Marketplace' employee can access the EnergyReport. \ No newline at end of file diff --git a/it/src/test/resources/policies/energyReport.json b/it/src/test/resources/policies/energyReport.json new file mode 100644 index 0000000..eb1c4e6 --- /dev/null +++ b/it/src/test/resources/policies/energyReport.json @@ -0,0 +1,37 @@ +{ + "@context": { + "dc": "http://purl.org/dc/elements/1.1/", + "dct": "http://purl.org/dc/terms/", + "owl": "http://www.w3.org/2002/07/owl#", + "odrl": "http://www.w3.org/ns/odrl/2/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "skos": "http://www.w3.org/2004/02/skos/core#" + }, + "@id": "https://mp-operation.org/policy/common/type", + "@type": "odrl:Policy", + "odrl:permission": { + "odrl:assigner": { + "@id": "https://www.mp-operation.org/" + }, + "odrl:target": { + "@type": "odrl:AssetCollection", + "odrl:source": "urn:asset", + "odrl:refinement": [ + { + "@type": "odrl:Constraint", + "odrl:leftOperand": "ngsi-ld:entityType", + "odrl:operator": { + "@id": "odrl:eq" + }, + "odrl:rightOperand": "EnergyReport" + } + ] + }, + "odrl:assignee": { + "@id": "odrl:any" + }, + "odrl:action": { + "@id": "odrl:read" + } + } +} diff --git a/k3s/consumer.yaml b/k3s/consumer.yaml new file mode 100644 index 0000000..9acb910 --- /dev/null +++ b/k3s/consumer.yaml @@ -0,0 +1,281 @@ +vcverifier: + enabled: false +credentials-config-service: + enabled: false +trusted-issuers-list: + enabled: false +mysql: + enabled: false +odrl-pap: + enabled: false +apisix: + enabled: false +scorpio: + enabled: false +postgis: + enabled: false + + +postgresql: + primary: + persistence: + enabled: false + readReplicas: + persistence: + enabled: false + +keycloak: + ingress: + enabled: true + hostname: keycloak-consumer.127.0.0.1.nip.io + args: + - -ec + - | + #!/bin/sh + export $(cat /did-material/did.env) + export KC_HOSTNAME=keycloak-consumer.127.0.0.1.nip.io + env | grep DID + /opt/keycloak/bin/kc.sh start --features oid4vc-vci --import-realm + initContainers: + - name: read-only-workaround + image: quay.io/wi_stefan/keycloak:25.0.0-PRE + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + cp -r /opt/keycloak/lib/quarkus/* /quarkus + volumeMounts: + - name: empty-dir + mountPath: /quarkus + subPath: app-quarkus-dir + + - name: get-did + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + apt-get -y update; apt-get -y install wget; apt-get -y install sed; + + cd /did-material + wget http://did-helper:3001/did-material/cert.pfx + wget http://did-helper:3001/did-material/did.env + mkdir -p /did-material/client + cd /did-material/client + wget http://did-helper.provider.svc.cluster.local:3002/did-material/did.env + sed -i -e 's/DID/CLIENT_DID/g' /did-material/client/did.env + echo "" >> /did-material/did.env + echo $(cat /did-material/client/did.env) >> /did-material/did.env + echo $(cat /did-material/did.env) + volumeMounts: + - name: did-material + mountPath: /did-material + + - name: register-at-tir + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + source /did-material/did.env + apt-get -y update; apt-get -y install curl + curl -X 'POST' 'http://tir.trust-anchor.svc.cluster.local:8080/issuer' -H 'Content-Type: application/json' -d "{\"did\": \"${DID}\", \"credentials\": []}" + volumeMounts: + - name: did-material + mountPath: /did-material + + - name: register-at-til + image: quay.io/curl/curl:8.1.2 + command: + - /bin/sh + args: + - -ec + - | + #!/bin/sh + export $(cat /did-material/did.env) + /bin/init.sh + volumeMounts: + - name: consumer-til-registration + mountPath: /bin/init.sh + subPath: init.sh + - name: did-material + mountPath: /did-material + + extraVolumes: + - name: did-material + emptyDir: { } + - name: qtm-temp + emptyDir: { } + - name: realms + configMap: + name: test-realm-realm + - name: consumer-til-registration + configMap: + name: consumer-til-registration + defaultMode: 0755 + realm: + frontendUrl: http://keycloak-consumer.127.0.0.1.nip.io:8080 + import: true + name: test-realm + clientRoles: | + "${CLIENT_DID}": [ + { + "name": "READER", + "description": "Is allowed to register", + "clientRole": true + }, + { + "name": "WRITER", + "description": "Is allowed to see", + "clientRole": true + } + ] + + users: | + { + "username": "test-user", + "enabled": true, + "email": "test@user.org", + "firstName": "Test", + "lastName": "Reader", + "credentials": [ + { + "type": "password", + "value": "test" + } + ], + "clientRoles": { + "${CLIENT_DID}": [ + "READER" + ], + "account": [ + "view-profile", + "manage-account" + ] + }, + "groups": [ + ] + } + clients: | + { + "clientId": "${CLIENT_DID}", + "enabled": true, + "description": "Client to connect test.org", + "surrogateAuthRequired": false, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "defaultRoles": [], + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "oid4vc", + "attributes": { + "client.secret.creation.time": "1675260539", + "vc.user-credential.format": "jwt_vc", + "vc.user-credential.scope": "UserCredential", + "vc.verifiable-credential.format": "jwt_vc", + "vc.verifiable-credential.scope": "VerifiableCredential" + }, + "protocolMappers": [ + { + "name": "target-role-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-target-role-mapper", + "config": { + "subjectProperty": "roles", + "clientId": "${CLIENT_DID}", + "supportedCredentialTypes": "VerifiableCredential" + } + }, + { + "name": "target-vc-role-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-target-role-mapper", + "config": { + "subjectProperty": "roles", + "clientId": "${CLIENT_DID}", + "supportedCredentialTypes": "UserCredential" + } + }, + { + "name": "context-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-context-mapper", + "config": { + "context": "https://www.w3.org/2018/credentials/v1", + "supportedCredentialTypes": "VerifiableCredential,UserCredential" + } + }, + { + "name": "email-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-user-attribute-mapper", + "config": { + "subjectProperty": "email", + "userAttribute": "email", + "supportedCredentialTypes": "UserCredential" + } + }, + { + "name": "firstName-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-user-attribute-mapper", + "config": { + "subjectProperty": "firstName", + "userAttribute": "firstName", + "supportedCredentialTypes": "UserCredential" + } + }, + { + "name": "lastName-mapper", + "protocol": "oid4vc", + "protocolMapper": "oid4vc-user-attribute-mapper", + "config": { + "subjectProperty": "lastName", + "userAttribute": "lastName", + "supportedCredentialTypes": "UserCredential" + } + } + ], + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [], + "optionalClientScopes": [] + } +did: + enabled: true + secret: issuance-secret + serviceType: ClusterIP + port: 3001 + cert: + country: BE + state: BRUSSELS + locality: Brussels + organization: Fancy Marketplace Co. + commonName: www.fancy-marketplace.biz + ingress: + enabled: true + host: did-consumer.127.0.0.1.nip.io + +# register the consumer at the til. Only possible if it runs in the same environment and recommendable for demo deployments +registration: + enabled: true + configMap: consumer-til-registration + til: http://trusted-issuers-list.provider.svc.cluster.local:8080 + did: ${DID} + credentialsType: UserCredential diff --git a/k3s/infra/coredns/coredns-cm.yaml b/k3s/infra/coredns/coredns-cm.yaml new file mode 100644 index 0000000..66539b0 --- /dev/null +++ b/k3s/infra/coredns/coredns-cm.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: coredns + namespace: kube-system +data: + Corefile: | + .:53 { + errors + health + ready + file /etc/coredns/nip.db 127.0.0.1.nip.io + kubernetes cluster.local in-addr.arpa ip6.arpa { + pods insecure + fallthrough in-addr.arpa ip6.arpa + } + hosts /etc/coredns/NodeHosts { + ttl 60 + reload 15s + fallthrough + } + prometheus :9153 + forward . /etc/resolv.conf + cache 30 + loop + reload + loadbalance + import /etc/coredns/custom/*.override + } + import /etc/coredns/custom/*.server + NodeHosts: | + 172.17.0.2 k3s + # in order to make the nip.io served local host addresses also available cluster internal(e.g. to the pods), we instruct coredns + # to forward all such requests inside the cluster to the traefik' loadbalancer-service + nip.db: | + 127.0.0.1.nip.io. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600 + 127.0.0.1.nip.io. IN NS a.iana-servers.net. + 127.0.0.1.nip.io. IN NS b.iana-servers.net. + *.127.0.0.1.nip.io. IN CNAME traefik-loadbalancer.infra.svc.cluster.local. \ No newline at end of file diff --git a/k3s/infra/coredns/deployment.yaml b/k3s/infra/coredns/deployment.yaml new file mode 100644 index 0000000..35f23ca --- /dev/null +++ b/k3s/infra/coredns/deployment.yaml @@ -0,0 +1,128 @@ +# overwrites the k3s default coredns deployment, in order to load the additional configuration +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + k8s-app: kube-dns + kubernetes.io/name: CoreDNS + name: coredns + namespace: kube-system +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 0 + selector: + matchLabels: + k8s-app: kube-dns + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + k8s-app: kube-dns + spec: + containers: + - args: + - -conf + - /etc/coredns/Corefile + image: rancher/mirrored-coredns-coredns:1.10.1 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /health + port: 8080 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: coredns + ports: + - containerPort: 53 + name: dns + protocol: UDP + - containerPort: 53 + name: dns-tcp + protocol: TCP + - containerPort: 9153 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: 8181 + scheme: HTTP + periodSeconds: 2 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + memory: 170Mi + requests: + cpu: 100m + memory: 70Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_BIND_SERVICE + drop: + - all + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /etc/coredns + name: config-volume + readOnly: true + - mountPath: /etc/coredns/custom + name: custom-config-volume + readOnly: true + dnsPolicy: Default + nodeSelector: + kubernetes.io/os: linux + priorityClassName: system-cluster-critical + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: coredns + serviceAccountName: coredns + terminationGracePeriodSeconds: 30 + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Exists + topologySpreadConstraints: + - labelSelector: + matchLabels: + k8s-app: kube-dns + maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + volumes: + - configMap: + defaultMode: 420 + items: + - key: Corefile + path: Corefile + - key: NodeHosts + path: NodeHosts + - key: nip.db + path: nip.db + name: coredns + name: config-volume + - configMap: + defaultMode: 420 + name: coredns-custom + optional: true + name: custom-config-volume \ No newline at end of file diff --git a/k3s/infra/traefik/deployment.yaml b/k3s/infra/traefik/deployment.yaml new file mode 100644 index 0000000..4190b3d --- /dev/null +++ b/k3s/infra/traefik/deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: traefik + namespace: infra + labels: + app.kubernetes.io/name: traefik +spec: + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: traefik + template: + metadata: + labels: + app.kubernetes.io/name: traefik + spec: + containers: + - name: traefik + image: traefik:v2.6.0 + imagePullPolicy: IfNotPresent + args: + - --providers.kubernetesingress=true + - --entrypoints.traefik.address=:8090 + - --entrypoints.http.address=:8080 + - --accesslog=true + - --accesslog.fields.defaultmode=keep + - --accesslog.fields.headers.defaultmode=keep + - --ping=true + - --api.insecure=true + - --api.dashboard=true + - --api.debug=true + - --global.checknewversion=false + - --global.sendAnonymousUsage=false + ports: + - name: http + containerPort: 8080 + - name: admin + containerPort: 8090 + startupProbe: + httpGet: + path: /ping + port: admin + initialDelaySeconds: 1 + periodSeconds: 1 + successThreshold: 1 + failureThreshold: 60 + readinessProbe: + httpGet: + path: /ping + port: admin + livenessProbe: + httpGet: + path: /ping + port: admin + securityContext: + runAsUser: 10001 + runAsGroup: 10001 + readOnlyRootFilesystem: true + serviceAccountName: traefik + terminationGracePeriodSeconds: 0 diff --git a/k3s/infra/traefik/ingress.yaml b/k3s/infra/traefik/ingress.yaml new file mode 100644 index 0000000..3b9e7d2 --- /dev/null +++ b/k3s/infra/traefik/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: traefik + namespace: infra + labels: + app.kubernetes.io/name: traefik +spec: + rules: + - host: traefik.127.0.0.1.nip.io + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: traefik + port: + name: admin diff --git a/k3s/infra/traefik/rbac.yaml b/k3s/infra/traefik/rbac.yaml new file mode 100644 index 0000000..11d109c --- /dev/null +++ b/k3s/infra/traefik/rbac.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: traefik + namespace: infra + labels: + app.kubernetes.io/name: traefik +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: traefik + namespace: infra + labels: + app.kubernetes.io/name: traefik +rules: + - apiGroups: + - "" + resources: + - services + - endpoints + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + - ingressclasses + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: traefik + namespace: infra + labels: + app.kubernetes.io/name: traefik +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: traefik +subjects: + - name: traefik + namespace: infra + kind: ServiceAccount diff --git a/k3s/infra/traefik/service.yaml b/k3s/infra/traefik/service.yaml new file mode 100644 index 0000000..8c9131f --- /dev/null +++ b/k3s/infra/traefik/service.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + name: traefik-admin + namespace: infra + labels: + app.kubernetes.io/name: traefik +spec: + selector: + app.kubernetes.io/name: traefik + ports: + - name: admin + port: 80 + targetPort: admin +--- +apiVersion: v1 +kind: Service +metadata: + name: traefik-loadbalancer + namespace: infra + labels: + app.kubernetes.io/name: traefik +spec: + type: LoadBalancer + selector: + app.kubernetes.io/name: traefik + ports: + - name: http + port: 8080 + targetPort: http diff --git a/k3s/namespaces/consumer.yaml b/k3s/namespaces/consumer.yaml new file mode 100644 index 0000000..f4661a3 --- /dev/null +++ b/k3s/namespaces/consumer.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: consumer \ No newline at end of file diff --git a/k3s/namespaces/infra.yaml b/k3s/namespaces/infra.yaml new file mode 100644 index 0000000..d48ab19 --- /dev/null +++ b/k3s/namespaces/infra.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: infra \ No newline at end of file diff --git a/k3s/namespaces/provider.yaml b/k3s/namespaces/provider.yaml new file mode 100644 index 0000000..df39dfe --- /dev/null +++ b/k3s/namespaces/provider.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: provider \ No newline at end of file diff --git a/k3s/namespaces/trust-anchor.yaml b/k3s/namespaces/trust-anchor.yaml new file mode 100644 index 0000000..53a4084 --- /dev/null +++ b/k3s/namespaces/trust-anchor.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: trust-anchor \ No newline at end of file diff --git a/k3s/namespaces/wallet.yaml b/k3s/namespaces/wallet.yaml new file mode 100644 index 0000000..bcb65a3 --- /dev/null +++ b/k3s/namespaces/wallet.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: wallet \ No newline at end of file diff --git a/k3s/provider.yaml b/k3s/provider.yaml new file mode 100644 index 0000000..136459a --- /dev/null +++ b/k3s/provider.yaml @@ -0,0 +1,193 @@ +keycloak: + enabled: false + +apisix: + image: + debug: true + dataPlane: + ingress: + enabled: true + hostname: mp-data-service.127.0.0.1.nip.io + catchAllRoute: + enabled: false + routes: |- + - uri: /.well-known/openid-configuration + upstream: + nodes: + verifier:3000: 1 + type: roundrobin + plugins: + proxy-rewrite: + uri: /services/data-service/.well-known/openid-configuration + - uri: /* + upstream: + nodes: + data-service-scorpio:9090: 1 + type: roundrobin + plugins: + openid-connect: + bearer_only: true + use_jwks: true + client_id: data-service + client_secret: unused + ssl_verify: false + discovery: http://verifier:3000/services/data-service/.well-known/openid-configuration + opa: + host: "http://localhost:8181" + policy: policy/main + +vcverifier: + ingress: + enabled: true + hosts: + - host: provider-verifier.127.0.0.1.nip.io + paths: + - "/" + deployment: + logging: + level: DEBUG + verifier: + tirAddress: http://tir.127.0.0.1.nip.io:8080/ + did: ${DID} + server: + host: http://provider-verifier.127.0.0.1.nip.io:8080 + configRepo: + configEndpoint: http://credentials-config-service:8080 + alternativeConfig: /alternative-conf/server.yaml + additionalVolumes: + - name: did-material + emptyDir: {} + - name: alternative-conf + emptyDir: {} + additionalVolumeMounts: + - name: alternative-conf + mountPath: /alternative-conf + initContainers: + - name: get-did + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + apt-get -y update; apt-get -y install wget; apt-get -y install gettext-base + cd /did-material + wget http://did-helper:3002/did-material/did.env + export $(cat /did-material/did.env) + cp /original-conf/server.yaml /alternative-conf/server.yaml + envsubst < /alternative-conf/server.yaml + volumeMounts: + - name: did-material + mountPath: /did-material + - name: config-volume + mountPath: /original-conf + - name: alternative-conf + mountPath: /alternative-conf + + - name: register-at-tir + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + source /did-material/did.env + apt-get -y update; apt-get -y install curl + curl -X 'POST' 'http://tir.trust-anchor.svc.cluster.local:8080/issuer' -H 'Content-Type: application/json' -d "{\"did\": \"${DID}\", \"credentials\": []}" + volumeMounts: + - name: did-material + mountPath: /did-material + +mysql: + primary: + persistence: + enabled: false + secondary: + persistence: + enabled: false + +postgis: + primary: + persistence: + enabled: false + readReplicas: + persistence: + enabled: false + +postgresql: + primary: + persistence: + enabled: false + readReplicas: + persistence: + enabled: false + +did: + enabled: true + secret: issuance-secret + serviceType: ClusterIP + port: 3002 + cert: + country: DE + state: SAXONY + locality: Dresden + organization: M&P Operations Inc. + commonName: www.mp-operation.org + ingress: + enabled: true + host: did-provider.127.0.0.1.nip.io + +scorpio: + ingress: + enabled: true + # only to make it available for the test initialization + hosts: + - host: scorpio-provider.127.0.0.1.nip.io + paths: + - "/" + ccs: + defaultOidcScope: + credentialType: UserCredential + trustedParticipantsLists: http://tir.trust-anchor.svc.cluster.local:8080 + +odrl-pap: + deployment: + initContainers: + - name: get-did + image: ubuntu + command: + - /bin/bash + args: + - -ec + - | + #!/bin/bash + apt-get -y update; apt-get -y install wget + cd /did-material + wget http://did-helper:3002/did-material/did.env + volumeMounts: + - name: did-material + mountPath: /did-material + additionalVolumes: + - name: did-material + emptyDir: {} + additionalVolumeMounts: + - name: did-material + mountPath: /did-material + command: + - /bin/sh + args: + - -ec + - | + #!/bin/sh + source /did-material/did.env + export GENERAL_ORGANIZATION_DID=$DID + ./application -Dquarkus.http.host=0.0.0.0 + + ingress: + enabled: true + hosts: + - host: pap-provider.127.0.0.1.nip.io + paths: + - "/" \ No newline at end of file diff --git a/k3s/trust-anchor.yaml b/k3s/trust-anchor.yaml new file mode 100644 index 0000000..c72dbba --- /dev/null +++ b/k3s/trust-anchor.yaml @@ -0,0 +1,19 @@ +trusted-issuers-list: + # -- it should work as the TrustedIssuersRegistry of the dataspace, thus we publish this api only + ingress: + tir: + enabled: true + hosts: + - host: tir.127.0.0.1.nip.io + til: + enabled: true + hosts: + - host: til.127.0.0.1.nip.io + +mysql: + primary: + persistence: + enabled: false + secondary: + persistence: + enabled: false \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..577f911 --- /dev/null +++ b/pom.xml @@ -0,0 +1,317 @@ + + + 4.0.0 + + org.fiware.dataspace + connector + 0.0.1 + pom + + it + + + + 17 + 17 + + UTF-8 + 1.2.4 + 6.13.0 + 3.1.1 + 2.4 + 3.3.0 + 3.1.1 + 3.0.0 + 3.0.0 + + ${project.basedir} + + + + + + src/main/resources + true + + + + + src/test/resources + true + + + + + org.apache.maven.plugins + maven-install-plugin + ${version.org.apache.maven.plugins.maven-install} + + + org.apache.maven.plugins + maven-source-plugin + ${version.org.apache.maven.plugins.maven-source} + + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.org.apache.maven.plugins.maven-dependency} + + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.org.apache.maven.plugins.maven-jar} + + true + + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.org.apache.maven.plugins.maven-surefire} + + + default-test + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.org.apache.maven.plugins.maven-failsafe} + + false + + **/RunCucumberTest.java + + + + + test + integration-test + + integration-test + + + + verify + integration-test + + verify + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + maven-resources-plugin + + true + + + + copy-resources-trust-anchor + prepare-package + + copy-resources + + + ${project.build.directory}/charts/trust-anchor + + + ${main.basedir}/charts/trust-anchor + + + + + + copy-resources-dsc + prepare-package + + copy-resources + + + ${project.build.directory}/charts + + + ${main.basedir}/charts + + + + + + copy-resources-namespaces + prepare-package + + copy-resources + + + ${project.build.directory}/k3s/namespaces + + + ${main.basedir}/k3s/namespaces + + + + + + copy-resources-infra + prepare-package + + copy-resources + + + ${project.build.directory}/k3s/infra + + + ${main.basedir}/k3s/infra + + + + + + + + + io.kokuwa.maven + helm-maven-plugin + ${version.io.kokuwa.helm-maven-plugin} + + true + + + + template-dsc-provider + + init + dependency-update + template + + package + + ${project.build.directory}/charts/data-space-connector + false + true + ${project.build.directory}/k3s/dsc-provider + --name-template=provider --namespace=provider -f ${main.basedir}/k3s/provider.yaml + + + + + template-dsc-consumer + + init + dependency-update + template + + package + + ${project.build.directory}/charts/data-space-connector + false + true + ${project.build.directory}/k3s/dsc-consumer + --name-template=consumer --namespace=consumer -f ${main.basedir}/k3s/consumer.yaml + + + + + template-trust-anchor + + init + dependency-update + template + + package + + ${project.build.directory}/charts/trust-anchor + false + true + ${project.build.directory}/k3s/trust-anchor + --name-template=trust-anchor --namespace=trust-anchor -f ${main.basedir}/k3s/trust-anchor.yaml + + + + + + + io.kokuwa.maven + k3s-maven-plugin + ${version.io.kokuwa.maven.k3s-plugin} + + true + + 8080:8080 + + + + + create-namespaces + deploy + + run + apply + + + false + ${project.build.directory}/k3s/namespaces + + + + apply-participants + deploy + + apply + + + ${project.build.directory}/k3s + 500 + + + + + + + + + local + + + + maven-resources-plugin + + false + + + + io.kokuwa.maven + helm-maven-plugin + + false + + + + io.kokuwa.maven + k3s-maven-plugin + + false + + + + + + + + + \ No newline at end of file