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: