From d282dc979781b2c42993aeb8a9f5ea9b9aa90d12 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 11 Nov 2026 13:36:37 +0100 Subject: [PATCH] feat: narrow the onboarding API's IssuerService scopes, bump platform The DECADE-X onboarding API registered holders and sent credential offers with a token exchanged under the platform's sudo mapping for issuer-admin-api:admin, which bypasses the IssuerService's ownership checks. It now exchanges its token under the issuer's own participant context, whose sub owns every path it addresses, and requests only issuer-admin-api:holders:read and :holders:write for the holder registration and :credentials:write for the offer. The chart's jwtlet mapping is narrowed to the same. Bumps core-platform-distribution to 0.0.41, which runs CFM 0.0.8 (the CFM agents also call the IssuerService under the issuer's context now) and seeds their issuer mapping, and the certo agent to 0.0.8. Co-Authored-By: Claude Opus 5.5 --- charts/cx-ve/Chart.lock | 6 +++--- charts/cx-ve/Chart.yaml | 2 +- charts/cx-ve/values.yaml | 2 +- .../dx-onboarding-api/templates/jwtlet-seed-job.yaml | 2 +- charts/dx-onboarding-api/values.yaml | 12 +++++++----- .../issuer/IssuerServiceCredentialOfferService.java | 7 ++++--- .../IssuerServiceHolderRegistrationService.java | 10 +++++----- .../dxonboarding/config/IssuerServiceProperties.java | 5 +++-- .../src/main/resources/application.yaml | 7 ++++--- .../out/issuer/IssuerServiceAdaptersTest.java | 6 +++--- vendor-stack/chart/Chart.lock | 6 +++--- vendor-stack/chart/Chart.yaml | 2 +- vendor-stack/chart/values.yaml | 2 +- 13 files changed, 37 insertions(+), 32 deletions(-) diff --git a/charts/cx-ve/Chart.lock b/charts/cx-ve/Chart.lock index e15ee3c..228552b 100644 --- a/charts/cx-ve/Chart.lock +++ b/charts/cx-ve/Chart.lock @@ -1,7 +1,7 @@ dependencies: - name: core-platform-distribution repository: oci://ghcr.io/eclipse-cfm/charts - version: 0.0.38 + version: 0.0.41 - name: dx-onboarding-api repository: file://../dx-onboarding-api version: 0.1.0 @@ -17,5 +17,5 @@ dependencies: - name: certo repository: oci://ghcr.io/metaform/charts version: 0.0.8 -digest: sha256:572fa35cd63d6ffa916f89bdd100e2507fdc3a5afe6e32fb715450a459a57204 -generated: "2026-11-06T11:53:18.747977922Z" +digest: sha256:4e7fd7061dfd44ca0ddc362c048d3f50b838724fa8986ede6ffa2acca1848169 +generated: "2026-11-11T13:03:30.05584+01:00" diff --git a/charts/cx-ve/Chart.yaml b/charts/cx-ve/Chart.yaml index cab5ead..2abf6a0 100644 --- a/charts/cx-ve/Chart.yaml +++ b/charts/cx-ve/Chart.yaml @@ -16,7 +16,7 @@ dependencies: # infra (NATS/Vault/Postgres/telemetry) + generic platform seeding - name: core-platform-distribution repository: oci://ghcr.io/eclipse-cfm/charts - version: 0.0.38 + version: 0.0.41 # The DECADE-X onboarding API (the TSP onboarding intake) from this repository - name: dx-onboarding-api repository: file://../dx-onboarding-api diff --git a/charts/cx-ve/values.yaml b/charts/cx-ve/values.yaml index 2f8454d..9bbf9ca 100644 --- a/charts/cx-ve/values.yaml +++ b/charts/cx-ve/values.yaml @@ -337,7 +337,7 @@ certoAgent: replicas: 1 image: repository: ghcr.io/eclipse-cfm/cfm/certoagent - tag: 0.0.6 + tag: 0.0.8 pullPolicy: IfNotPresent # The agent runs under the platform's "cfm-agents" ServiceAccount; the certo-jwtlet-seed job # maps that SA to the "sudo" participant context with the certo-mgmt-api scopes, authorizing diff --git a/charts/dx-onboarding-api/templates/jwtlet-seed-job.yaml b/charts/dx-onboarding-api/templates/jwtlet-seed-job.yaml index 37d5b54..84e0221 100644 --- a/charts/dx-onboarding-api/templates/jwtlet-seed-job.yaml +++ b/charts/dx-onboarding-api/templates/jwtlet-seed-job.yaml @@ -1,7 +1,7 @@ ## Registers this app's ServiceAccount in JWTlet so the pod's projected SA token can be exchanged ## (RFC 8693) for the token its IssuerService calls need: on approval, the TSP registers the ## participant as credential holder and has the issuer offer it its credentials, through the -## issuer's admin API (`sudo` with issuer-admin-api:admin). The scope mappings themselves are seeded by the platform; this job only +## issuer's admin API (the `issuer` context with the holders and credentials:write scopes). The scope mappings themselves are seeded by the platform; this job only ## registers the SA. Creating a mapping is idempotent (409 = already exists). {{- if .Values.jwtlet.seed.enabled }} apiVersion: batch/v1 diff --git a/charts/dx-onboarding-api/values.yaml b/charts/dx-onboarding-api/values.yaml index a26af2f..66c9290 100644 --- a/charts/dx-onboarding-api/values.yaml +++ b/charts/dx-onboarding-api/values.yaml @@ -51,11 +51,13 @@ jwtlet: enabled: true mappings: # IssuerService admin API (holder registration, credential offers) — the app's only exchange. - # Must agree with config issuer-service.token-resource; `sudo` is no participant context the - # issuer knows, which is why the admin scope is what lets the calls through. - - participantContext: sudo + # Must agree with config issuer-service.token-resource. Exchanging under the issuer's own + # context makes the token's sub own it, so the resource scopes suffice and no admin is needed. + - participantContext: issuer scopes: - - issuer-admin-api:admin + - issuer-admin-api:holders:read + - issuer-admin-api:holders:write + - issuer-admin-api:credentials:write audiences: - edcv image: curlimages/curl:latest @@ -81,7 +83,7 @@ config: issuer-service: url: http://issuerservice.edc-v.svc.cluster.local:10013/api/admin issuer-context-id: issuer - token-resource: sudo + token-resource: issuer credential-definitions: legal-entity: dx-legal-entity-credential-def site: dx-site-credential-def diff --git a/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceCredentialOfferService.java b/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceCredentialOfferService.java index c36e1b8..9f4b188 100644 --- a/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceCredentialOfferService.java +++ b/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceCredentialOfferService.java @@ -14,7 +14,7 @@ /** * Has the IssuerService offer the holder its DECADE-X credentials, authorized like the holder - * registration: the Legal Entity VC, and the Site VC when the participant declared a business site + * registration but with {@code issuer-admin-api:credentials:write}: the Legal Entity VC, and the Site VC when the participant declared a business site * ({@code issuer-service.credential-definitions}). The IssuerService resolves the holder's DID * document and pushes a DCP CredentialOffer to the CredentialService it advertises; an unknown * definition id fails the whole offer. @@ -22,6 +22,8 @@ @Component public class IssuerServiceCredentialOfferService implements CredentialOfferService { + static final String SCOPE = "issuer-admin-api:credentials:write"; + private static final Logger log = LoggerFactory.getLogger(IssuerServiceCredentialOfferService.class); private final RestClient restClient; @@ -46,8 +48,7 @@ public void offerCredentials(OnboardingRequest request) { DecadeXCredentialClaims.firstSite(request).ifPresent(site -> offered.add(definitions.site())); restClient.post() .uri("/v1/participants/{issuerContextId}/credentials/offer", properties.issuerContextId()) - .header("Authorization", "Bearer " + tokenProvider.getToken(properties.tokenResource(), - IssuerServiceHolderRegistrationService.SCOPE)) + .header("Authorization", "Bearer " + tokenProvider.getToken(properties.tokenResource(), SCOPE)) .body(Map.of("holderId", holderId, "credentials", offered)) .retrieve() .toBodilessEntity(); diff --git a/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceHolderRegistrationService.java b/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceHolderRegistrationService.java index 73c844c..77332ad 100644 --- a/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceHolderRegistrationService.java +++ b/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceHolderRegistrationService.java @@ -26,15 +26,15 @@ * properties — instead of being taken as success, which would leave the holder without the * DECADE-X claims and fail the credentials' generation later, out of sight. * - *

Authorized by a workload token exchanged under the - * {@code issuer-service.token-resource} mapping (the platform's {@code sudo}) for - * {@code issuer-admin-api:admin} — the scope the admin API accepts from a caller that is not one of - * its participants. + *

Authorized by a workload token exchanged under the {@code issuer-service.token-resource} + * mapping, which must be the issuer's participant context: the token's {@code sub} then owns the + * addressed context, so the holder scopes suffice and no {@code issuer-admin-api:admin} elevation is + * needed. The GET of the 409 merge needs {@code holders:read}, the POST and PUT {@code holders:write}. */ @Component public class IssuerServiceHolderRegistrationService implements HolderRegistrationService { - static final String SCOPE = "issuer-admin-api:admin"; + static final String SCOPE = "issuer-admin-api:holders:read issuer-admin-api:holders:write"; private static final Logger log = LoggerFactory.getLogger(IssuerServiceHolderRegistrationService.class); diff --git a/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/config/IssuerServiceProperties.java b/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/config/IssuerServiceProperties.java index 0989020..5ac2178 100644 --- a/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/config/IssuerServiceProperties.java +++ b/dx-onboarding-api/src/main/java/com/metaform/dxonboarding/config/IssuerServiceProperties.java @@ -8,8 +8,9 @@ * * @param url the admin API's base URL (in-cluster: the issuer's admin port) * @param issuerContextId the issuer's participant context, which holds the credential definitions - * @param tokenResource the jwtlet mapping the workload token is exchanged under — it must - * grant {@code issuer-admin-api:admin} + * @param tokenResource the jwtlet mapping the workload token is exchanged under — the + * issuer's participant context, granting {@code issuer-admin-api:holders:read}, + * {@code :holders:write} and {@code :credentials:write} * @param credentialDefinitions the credential definitions a DECADE-X member is offered */ @ConfigurationProperties(prefix = "issuer-service") diff --git a/dx-onboarding-api/src/main/resources/application.yaml b/dx-onboarding-api/src/main/resources/application.yaml index 3df5c55..50e992f 100644 --- a/dx-onboarding-api/src/main/resources/application.yaml +++ b/dx-onboarding-api/src/main/resources/application.yaml @@ -23,12 +23,13 @@ spring: # The IssuerService an approved participant is registered with as credential holder, and offered # its DECADE-X credentials through. The admin API is called -# with a token exchanged under the token-resource jwtlet mapping, which must grant -# issuer-admin-api:admin (the chart's jwtlet seed registers it for this service's account). +# with a token exchanged under the token-resource jwtlet mapping: the issuer's participant context, +# granting issuer-admin-api:holders:read, :holders:write and :credentials:write (the chart's jwtlet +# seed registers it for this service's account). issuer-service: url: http://issuerservice.edc-v.svc.cluster.local:10013/api/admin # ISSUERSERVICE_URL issuer-context-id: issuer # ISSUERSERVICE_ISSUERCONTEXTID - token-resource: sudo # ISSUERSERVICE_TOKENRESOURCE + token-resource: issuer # ISSUERSERVICE_TOKENRESOURCE # Must exist at the issuer — the umbrella chart's DECADE-X issuer seed creates them; an unknown id # fails the whole offer. Every member is offered the Legal Entity VC; the Site VC only a member # that declared a business site (it is issued for the first one). diff --git a/dx-onboarding-api/src/test/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceAdaptersTest.java b/dx-onboarding-api/src/test/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceAdaptersTest.java index b943185..b361bfb 100644 --- a/dx-onboarding-api/src/test/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceAdaptersTest.java +++ b/dx-onboarding-api/src/test/java/com/metaform/dxonboarding/adapter/out/issuer/IssuerServiceAdaptersTest.java @@ -35,7 +35,7 @@ class IssuerServiceAdaptersTest { private final RestClient.Builder builder = RestClient.builder().baseUrl(API); private final MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build(); private final RestClient restClient = builder.build(); - private final IssuerServiceProperties properties = new IssuerServiceProperties(API, "issuer", "sudo", + private final IssuerServiceProperties properties = new IssuerServiceProperties(API, "issuer", "issuer", new IssuerServiceProperties.CredentialDefinitions("dx-legal-entity-credential-def", "dx-site-credential-def")); private final IssuerServiceHolderRegistrationService holders = new IssuerServiceHolderRegistrationService( @@ -88,7 +88,7 @@ private static OnboardingRequestData withSites() { void registerHolder_registersTheDidWithItsLegalEntity() { server.expect(requestTo(API + "/v1/participants/issuer/holders")) .andExpect(method(HttpMethod.POST)) - .andExpect(header("Authorization", "Bearer token-for-sudo-issuer-admin-api:admin")) + .andExpect(header("Authorization", "Bearer token-for-issuer-issuer-admin-api:holders:read issuer-admin-api:holders:write")) .andExpect(content().json(""" {"holderId": "%s", "did": "%s", "name": "Acme Corp", "properties": {"id": "%s", "dxSubjectType": "dx:Wallet", "dxLegalEntity": %s}}""" @@ -174,7 +174,7 @@ void registerHolder_mergesIntoAHolderAnotherDataspaceRegistered() { void offerCredentials_offersBothCredentialsToAHolderWithASite() { server.expect(requestTo(API + "/v1/participants/issuer/credentials/offer")) .andExpect(method(HttpMethod.POST)) - .andExpect(header("Authorization", "Bearer token-for-sudo-issuer-admin-api:admin")) + .andExpect(header("Authorization", "Bearer token-for-issuer-issuer-admin-api:credentials:write")) .andExpect(content().json(""" {"holderId": "%s", "credentials": ["dx-legal-entity-credential-def", "dx-site-credential-def"]}""" .formatted(DID), true)) diff --git a/vendor-stack/chart/Chart.lock b/vendor-stack/chart/Chart.lock index bdaaccc..62a2ca6 100644 --- a/vendor-stack/chart/Chart.lock +++ b/vendor-stack/chart/Chart.lock @@ -1,12 +1,12 @@ dependencies: - name: core-platform-distribution repository: oci://ghcr.io/eclipse-cfm/charts - version: 0.0.38 + version: 0.0.41 - name: decadex-profile repository: file://../../charts/decadex-profile version: 0.1.0 - name: certo repository: oci://ghcr.io/metaform/charts version: 0.0.8 -digest: sha256:522471d02af7e32ef93bfa7ddd0a97d1d71f67759ac79acb60a343d1410d2c90 -generated: "2026-11-06T11:53:20.902352907Z" +digest: sha256:8637883c863e420a3560204e4bc3fb253d724b12a94d58abca913db0908f349f +generated: "2026-11-11T13:03:33.496487+01:00" diff --git a/vendor-stack/chart/Chart.yaml b/vendor-stack/chart/Chart.yaml index e61a989..1fe1dcd 100644 --- a/vendor-stack/chart/Chart.yaml +++ b/vendor-stack/chart/Chart.yaml @@ -20,7 +20,7 @@ dependencies: # infra (NATS/Vault/Postgres/telemetry) + generic platform seeding - name: core-platform-distribution repository: oci://ghcr.io/eclipse-cfm/charts - version: 0.0.38 + version: 0.0.41 # DECADE-X dataspace seeding from this repository: the EDC dataspace profile + DCP scope, and the # tenant-manager dataspace profile participant provisioning needs - name: decadex-profile diff --git a/vendor-stack/chart/values.yaml b/vendor-stack/chart/values.yaml index 0b6c155..e71bb75 100644 --- a/vendor-stack/chart/values.yaml +++ b/vendor-stack/chart/values.yaml @@ -170,7 +170,7 @@ certoAgent: replicas: 1 image: repository: ghcr.io/eclipse-cfm/cfm/certoagent - tag: 0.0.6 + tag: 0.0.8 pullPolicy: IfNotPresent resources: {} natsAuth: