From beb79d49cade4100bd9b651f1d4f06934ffd0f66 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Mon, 15 Jun 2026 10:38:13 +0200 Subject: [PATCH 1/2] feat: switch from OAuth2 token provider to workload token exchange Replaces the OAuth2-based token provider with a WorkloadTokenProvider that uses token exchange, and updates all API clients and tests accordingly. Co-Authored-By: Claude Sonnet 4.6 --- .../service/OAuth2TokenProvider.java | 45 --- .../application/service/TokenProvider.java | 2 +- .../service/WorkloadTokenProvider.java | 78 ++++ .../dataplane/DataPlaneApiClientImpl.java | 9 +- .../client/identityhub/IdentityHubClient.java | 8 - .../identityhub/IdentityHubClientImpl.java | 51 +-- .../management/ManagementApiClientImpl.java | 9 +- .../v1alpha1/TenantManagerClientImpl.java | 46 ++- .../config/TokenExchangeConfig.java | 33 ++ .../api/controller/EdcDataControllerTest.java | 5 +- .../TenantControllerIntegrationTest.java | 25 +- .../service/OAuth2TokenProviderTest.java | 339 ------------------ .../DataAccessServiceIntegrationTest.java | 4 +- .../service/TenantServiceIntegrationTest.java | 4 +- .../DataPlaneApiClientIntegrationTest.java | 4 +- .../IdentityHubClientImplTest.java | 132 +------ .../ManagementApiClientIntegrationTest.java | 10 +- .../TenantManagerClientImplTest.java | 6 +- 18 files changed, 182 insertions(+), 628 deletions(-) delete mode 100644 src/main/java/com/metaformsystems/redline/application/service/OAuth2TokenProvider.java create mode 100644 src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java create mode 100644 src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java delete mode 100644 src/test/java/com/metaformsystems/redline/application/service/OAuth2TokenProviderTest.java diff --git a/src/main/java/com/metaformsystems/redline/application/service/OAuth2TokenProvider.java b/src/main/java/com/metaformsystems/redline/application/service/OAuth2TokenProvider.java deleted file mode 100644 index 5c649ef..0000000 --- a/src/main/java/com/metaformsystems/redline/application/service/OAuth2TokenProvider.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2026 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package com.metaformsystems.redline.application.service; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; - -@Component -public class OAuth2TokenProvider implements TokenProvider { - - private final WebClient keycloakTokenClient; - - public OAuth2TokenProvider(WebClient keycloakTokenClient) { - this.keycloakTokenClient = keycloakTokenClient; - } - - @Override - public String getToken(String clientId, String clientSecret, String scopes) { - var tokenResponse = keycloakTokenClient.post() - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .bodyValue("grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret + "&scope=" + scopes) - .retrieve() - .bodyToMono(TokenResponse.class) - .block(); - - return tokenResponse != null ? tokenResponse.accessToken() : null; - } - - private record TokenResponse(@JsonProperty("access_token") String accessToken) { - } -} diff --git a/src/main/java/com/metaformsystems/redline/application/service/TokenProvider.java b/src/main/java/com/metaformsystems/redline/application/service/TokenProvider.java index cf2ba3a..435f04b 100644 --- a/src/main/java/com/metaformsystems/redline/application/service/TokenProvider.java +++ b/src/main/java/com/metaformsystems/redline/application/service/TokenProvider.java @@ -15,5 +15,5 @@ package com.metaformsystems.redline.application.service; public interface TokenProvider { - String getToken(String clientId, String clientSecret, String scopes); + String getToken(String participantContextId, String scopes); } diff --git a/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java b/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java new file mode 100644 index 0000000..b0e6ade --- /dev/null +++ b/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package com.metaformsystems.redline.application.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import static java.util.Optional.ofNullable; + +/** + * This provider reads a workload token from a file share, provided by Kubernetes, and exchanges it for a scoped token + * using the Token Exchange Protocol (RFC 8693 - OAuth 2.0 Token Exchange). + */ +@Component("token-exchange") +public class WorkloadTokenProvider implements TokenProvider { + private final String tokenFilePath; + private final String tokenExchangeAudience; + private final String defaultResource; + private final WebClient webClient; + + public WorkloadTokenProvider(@Value("${token.file.path:/var/run/secrets/jwtlet/token}") String tokenFilePath, + @Value("${token.exchange.audience:edcv}") String tokenExchangeAudience, + @Value("${token.exchange.resource:redline}") String defaultResource, + @Qualifier("tokenExchangeClient") WebClient webClient) { + this.tokenFilePath = tokenFilePath; + this.tokenExchangeAudience = tokenExchangeAudience; + this.defaultResource = defaultResource; + this.webClient = webClient; + } + + @Override + public String getToken(String resource, String scopes) { + try { + var tokenContent = Files.readString(Path.of(tokenFilePath)); + + var response = webClient.post() + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + .with("subject_token", tokenContent) + .with("audience", tokenExchangeAudience) + .with("resource", ofNullable(resource).orElse(defaultResource)) + .with("scope", scopes)) + .retrieve() + .bodyToMono(TokenResponse.class) + .block(); + + return Objects.requireNonNull(response).accessToken(); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private record TokenResponse(@JsonProperty("access_token") String accessToken) { + } +} diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientImpl.java b/src/main/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientImpl.java index 892379b..b41ba79 100644 --- a/src/main/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientImpl.java +++ b/src/main/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientImpl.java @@ -19,6 +19,7 @@ import com.metaformsystems.redline.domain.repository.ParticipantRepository; import com.metaformsystems.redline.infrastructure.client.dataplane.dto.UploadResponse; import com.metaformsystems.redline.infrastructure.client.management.dto.QuerySpec; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.buffer.DataBuffer; @@ -42,7 +43,8 @@ public class DataPlaneApiClientImpl implements DataPlaneApiClient { private final ParticipantRepository participantRepository; private final TokenProvider tokenProvider; - public DataPlaneApiClientImpl(WebClient dataPlanePublicClient, WebClient dataPlaneInternalClient, ParticipantRepository participantRepository, TokenProvider tokenProvider) { + public DataPlaneApiClientImpl(WebClient dataPlanePublicClient, WebClient dataPlaneInternalClient, ParticipantRepository participantRepository, + @Qualifier("token-exchange") TokenProvider tokenProvider) { this.dataPlanePublicClient = dataPlanePublicClient.mutate() .exchangeStrategies(ExchangeStrategies.builder() .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(50 * 1024 * 1024)) @@ -118,9 +120,8 @@ public byte[] downloadFile(String authToken, String fileId) { } private String getToken(String participantContextId) { - var participantProfile = participantRepository.findByParticipantContextId(participantContextId) + participantRepository.findByParticipantContextId(participantContextId) .orElseThrow(() -> new ObjectNotFoundException("Participant not found with context id: " + participantContextId)); - - return tokenProvider.getToken(participantProfile.getClientCredentials().clientId(), participantProfile.getClientCredentials().clientSecret(), "management-api:write management-api:read"); + return tokenProvider.getToken(participantContextId, "read write"); } } diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClient.java b/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClient.java index 9a7a9fc..892391e 100644 --- a/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClient.java +++ b/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClient.java @@ -28,23 +28,15 @@ */ public interface IdentityHubClient { - // Participant Context operations - List getAllParticipants(); IdentityHubParticipantContext getParticipant(String participantContextId); - // Verifiable Credentials operations - List getAllCredentials(); - List queryCredentialsByType(String participantContextId, String type); VerifiableCredentialResource getCredentialRequest(String participantContextId, String holderPid); void requestCredential(String participantContextId, CredentialRequestDto request); - // Key Pairs operations - List getAllKeyPairs(); - List queryKeyPairByParticipantContextId(String participantContextId); KeyPairResource getKeyPair(String participantContextId, String keyPairId); diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImpl.java b/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImpl.java index 46e3f4d..42b88c3 100644 --- a/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImpl.java +++ b/src/main/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImpl.java @@ -23,6 +23,7 @@ import com.metaformsystems.redline.infrastructure.client.identityhub.dto.KeyDescriptor; import com.metaformsystems.redline.infrastructure.client.identityhub.dto.KeyPairResource; import com.metaformsystems.redline.infrastructure.client.identityhub.dto.VerifiableCredentialResource; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Component; @@ -43,7 +44,7 @@ public class IdentityHubClientImpl implements IdentityHubClient { private final ParticipantRepository participantRepository; public IdentityHubClientImpl(WebClient identityHubWebClient, - TokenProvider tokenProvider, + @Qualifier("token-exchange") TokenProvider tokenProvider, ParticipantRepository participantRepository, @Value("${edc.api.clientId:provisioner}") String provisionerClientId, @Value("${edc.api.clientsecret:provisioner-secret}") String provisionerClientSecret) { @@ -54,19 +55,6 @@ public IdentityHubClientImpl(WebClient identityHubWebClient, this.participantRepository = participantRepository; } - @Override - public List getAllParticipants() { - return webClient.get() - .uri(uriBuilder -> uriBuilder - .path(IDENTITY_API_BASE + "/participants") - .build()) - .header("Authorization", "Bearer " + getToken()) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(); - } - @Override public IdentityHubParticipantContext getParticipant(String participantContextId) { return webClient.get() @@ -77,19 +65,6 @@ public IdentityHubParticipantContext getParticipant(String participantContextId) .block(); } - @Override - public List getAllCredentials() { - return webClient.get() - .uri(uriBuilder -> uriBuilder - .path(IDENTITY_API_BASE + "/credentials") - .build()) - .header("Authorization", "Bearer " + getToken()) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(); - } - @Override public List queryCredentialsByType(String participantContextId, String type) { return webClient.get() @@ -129,19 +104,6 @@ public void requestCredential(String participantContextId, CredentialRequestDto .block(); } - @Override - public List getAllKeyPairs() { - return webClient.get() - .uri(uriBuilder -> uriBuilder - .path(IDENTITY_API_BASE + "/keypairs") - .build()) - .header("Authorization", "Bearer " + getToken()) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(); - } - @Override public List queryKeyPairByParticipantContextId(String participantContextId) { return webClient.get() @@ -221,15 +183,10 @@ public void getDidState(String participantContextId, DidRequestPayload payload) .block(); } - private String getToken() { - return tokenProvider.getToken(provisionerClientId, provisionerClientSecret, "identity-api:read"); - } - private String getToken(String participantContextId) { - var participantProfile = participantRepository.findByParticipantContextId(participantContextId) + participantRepository.findByParticipantContextId(participantContextId) .orElseThrow(() -> new ObjectNotFoundException("Participant not found with context id: " + participantContextId)); - var token = tokenProvider.getToken(participantProfile.getClientCredentials().clientId(), participantProfile.getClientCredentials().clientSecret(), "identity-api:write identity-api:read"); - return token; + return tokenProvider.getToken(participantContextId, "read write"); } } diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientImpl.java b/src/main/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientImpl.java index 9044f28..0b3203f 100644 --- a/src/main/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientImpl.java +++ b/src/main/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientImpl.java @@ -35,6 +35,7 @@ import com.metaformsystems.redline.infrastructure.client.management.dto.TransferRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; @@ -57,7 +58,7 @@ public class ManagementApiClientImpl implements ManagementApiClient { private final ClientCredentials provisionerCredentials; public ManagementApiClientImpl(WebClient controlPlaneWebClient, - TokenProvider tokenProvider, + @Qualifier("token-exchange") TokenProvider tokenProvider, ParticipantRepository participantRepository, ObjectMapper objectMapper, @Value("${edc.api.clientId:provisioner}") String adminClientId, @@ -215,7 +216,7 @@ public List> queryContractNegotiations(String participantCon @Override public void createCelExpression(CelExpression celExpression) { - var token = tokenProvider.getToken(provisionerCredentials.clientId(), provisionerCredentials.clientSecret(), "management-api:write management-api:read"); + var token = tokenProvider.getToken(null, "management-api:write management-api:read"); controlPlaneWebClient.post() .uri("/v5beta/celexpressions") .header("Authorization", "Bearer %s".formatted(token)) @@ -320,10 +321,10 @@ public ContractAgreement getAgreement(String participantContextId, String negoti } private String getToken(String participantContextId) { - var participantProfile = participantRepository.findByParticipantContextId(participantContextId) + participantRepository.findByParticipantContextId(participantContextId) .orElseThrow(() -> new ObjectNotFoundException("Participant not found with context id: " + participantContextId)); - return tokenProvider.getToken(participantProfile.getClientCredentials().clientId(), participantProfile.getClientCredentials().clientSecret(), "management-api:write management-api:read"); + return tokenProvider.getToken(participantContextId, "read write"); } } diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/v1alpha1/TenantManagerClientImpl.java b/src/main/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/v1alpha1/TenantManagerClientImpl.java index d487760..c93ca5e 100644 --- a/src/main/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/v1alpha1/TenantManagerClientImpl.java +++ b/src/main/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/v1alpha1/TenantManagerClientImpl.java @@ -23,7 +23,7 @@ import com.metaformsystems.redline.infrastructure.client.tenantmanager.v1alpha1.dto.Tenant; import com.metaformsystems.redline.infrastructure.client.tenantmanager.v1alpha1.dto.TenantCreationRequest; import com.metaformsystems.redline.infrastructure.client.tenantmanager.v1alpha1.dto.TenantPropertiesDiff; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; @@ -33,26 +33,22 @@ @Component public class TenantManagerClientImpl implements TenantManagerClient { + public static final String TM_API_READ_SCOPE = "cfm-read"; + public static final String TM_API_WRITE_SCOPE = "cfm-write"; private static final String API_BASE = "/api/v1alpha1"; private final WebClient webClient; private final TokenProvider tokenProvider; - private final String provisionerClientId; - private final String provisionerClientSecret; - public TenantManagerClientImpl(WebClient tenantManagerWebClient, TokenProvider tokenProvider, - @Value("${edc.api.clientId:provisioner}") String provisionerClientId, - @Value("${edc.api.clientsecret:provisioner-secret}") String provisionerClientSecret) { + public TenantManagerClientImpl(WebClient tenantManagerWebClient, @Qualifier("token-exchange") TokenProvider tokenProvider) { this.webClient = tenantManagerWebClient; this.tokenProvider = tokenProvider; - this.provisionerClientId = provisionerClientId; - this.provisionerClientSecret = provisionerClientSecret; } @Override public List listCells() { return webClient.get() .uri(API_BASE + "/cells") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) @@ -63,7 +59,7 @@ public List listCells() { public Cell createCell(CellCreationRequest cellCreationRequest) { return webClient.post() .uri(API_BASE + "/cells") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .bodyValue(cellCreationRequest) .retrieve() .bodyToMono(Cell.class) @@ -74,7 +70,7 @@ public Cell createCell(CellCreationRequest cellCreationRequest) { public List listDataspaceProfiles() { return webClient.get() .uri(API_BASE + "/dataspace-profiles") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) @@ -85,7 +81,7 @@ public List listDataspaceProfiles() { public DataspaceProfile getDataspaceProfile(String id) { return webClient.get() .uri(API_BASE + "/dataspace-profiles/{id}", id) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(DataspaceProfile.class) .block(); @@ -95,7 +91,7 @@ public DataspaceProfile getDataspaceProfile(String id) { public void deployDataspaceProfile(String id) { webClient.post() .uri(API_BASE + "/dataspace-profiles/{id}/deployments", id) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .retrieve() .toBodilessEntity() .block(); @@ -105,7 +101,7 @@ public void deployDataspaceProfile(String id) { public List queryParticipantProfiles(ModelQuery query) { return webClient.post() .uri(API_BASE + "/participant-profiles/query") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .bodyValue(query) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { @@ -117,7 +113,7 @@ public List queryParticipantProfiles(ModelQuery query) { public List listParticipantProfiles(String tenantId) { return webClient.get() .uri(API_BASE + "/tenants/{id}/participant-profiles", tenantId) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) @@ -128,7 +124,7 @@ public List listParticipantProfiles(String tenantId) { public ParticipantProfile getParticipantProfile(String tenantId, String participantId) { return webClient.get() .uri(API_BASE + "/tenants/{id}/participant-profiles/{participantID}", tenantId, participantId) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(ParticipantProfile.class) .block(); @@ -138,7 +134,7 @@ public ParticipantProfile getParticipantProfile(String tenantId, String particip public ParticipantProfile deployParticipantProfile(String tenantId, ParticipantProfile profile) { return webClient.post() .uri(API_BASE + "/tenants/{id}/participant-profiles", tenantId) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .bodyValue(profile) .retrieve() .bodyToMono(ParticipantProfile.class) @@ -149,7 +145,7 @@ public ParticipantProfile deployParticipantProfile(String tenantId, ParticipantP public ParticipantProfile deleteParticipantProfile(String tenantId, String participantId) { return webClient.delete() .uri(API_BASE + "/tenants/{id}/participant-profiles/{participantID}", tenantId, participantId) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .retrieve() .bodyToMono(ParticipantProfile.class) .block(); @@ -159,7 +155,7 @@ public ParticipantProfile deleteParticipantProfile(String tenantId, String parti public List listTenants() { return webClient.get() .uri(API_BASE + "/tenants") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) @@ -170,7 +166,7 @@ public List listTenants() { public Tenant getTenant(String id) { return webClient.get() .uri(API_BASE + "/tenants/{id}", id) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .retrieve() .bodyToMono(Tenant.class) .block(); @@ -180,7 +176,7 @@ public Tenant getTenant(String id) { public Tenant createTenant(TenantCreationRequest newTenant) { return webClient.post() .uri(API_BASE + "/tenants") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .bodyValue(newTenant) .retrieve() .bodyToMono(Tenant.class) @@ -191,7 +187,7 @@ public Tenant createTenant(TenantCreationRequest newTenant) { public Tenant updateTenant(String id, TenantPropertiesDiff diff) { return webClient.patch() .uri(API_BASE + "/tenants/{id}", id) - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_WRITE_SCOPE)) .bodyValue(diff) .retrieve() .bodyToMono(Tenant.class) @@ -202,7 +198,7 @@ public Tenant updateTenant(String id, TenantPropertiesDiff diff) { public List queryTenants(ModelQuery query) { return webClient.post() .uri(API_BASE + "/tenants/query") - .header("Authorization", "Bearer " + getToken()) + .header("Authorization", "Bearer " + getToken(TM_API_READ_SCOPE)) .bodyValue(query) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { @@ -210,7 +206,7 @@ public List queryTenants(ModelQuery query) { .block(); } - private String getToken() { - return tokenProvider.getToken(provisionerClientId, provisionerClientSecret, "tenant-manager-api:read"); + private String getToken(String scope) { + return tokenProvider.getToken(null, scope); } } diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java b/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java new file mode 100644 index 0000000..34bda2c --- /dev/null +++ b/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package com.metaformsystems.redline.infrastructure.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class TokenExchangeConfig { + @Value("${token.exchange.url:http://localhost:8080/token}") + private String tokenExchangeUrl; + + @Bean + public WebClient tokenExchangeClient(WebClient.Builder webClientBuilder) { + return webClientBuilder + .baseUrl(tokenExchangeUrl) + .build(); + } +} diff --git a/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java b/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java index cb2c053..90de9eb 100644 --- a/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java +++ b/src/test/java/com/metaformsystems/redline/api/controller/EdcDataControllerTest.java @@ -98,7 +98,7 @@ public class EdcDataControllerTest { @Autowired private ParticipantRepository participantRepository; - @MockitoBean + @MockitoBean("token-exchange") private TokenProvider tokenProvider; private ServiceProvider serviceProvider; @@ -111,7 +111,6 @@ static void configureProperties(DynamicPropertyRegistry registry) throws IOExcep registry.add("dataplane.url", () -> "http://%s:%s/dataplane".formatted(mockBackEndHost, mockBackEndPort)); registry.add("dataplane.internal.url", () -> "http://%s:%s/dataplane".formatted(mockBackEndHost, mockBackEndPort)); registry.add("controlplane.url", () -> "http://%s:%s/controlplane".formatted(mockBackEndHost, mockBackEndPort)); - } @AfterEach @@ -134,7 +133,7 @@ void setUp() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(InetAddress.getByName(mockBackEndHost), mockBackEndPort); - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("test-token"); + when(tokenProvider.getToken(anyString(), anyString())).thenReturn("test-token"); when(webDidResolver.resolveProtocolEndpoints(anyString())).thenReturn("http://example.com/api"); } diff --git a/src/test/java/com/metaformsystems/redline/api/controller/TenantControllerIntegrationTest.java b/src/test/java/com/metaformsystems/redline/api/controller/TenantControllerIntegrationTest.java index a69ef99..0ad299e 100644 --- a/src/test/java/com/metaformsystems/redline/api/controller/TenantControllerIntegrationTest.java +++ b/src/test/java/com/metaformsystems/redline/api/controller/TenantControllerIntegrationTest.java @@ -94,20 +94,19 @@ class TenantControllerIntegrationTest { @Autowired private ParticipantRepository participantRepository; - @MockitoBean + @MockitoBean("token-exchange") private TokenProvider tokenProvider; private com.metaformsystems.redline.domain.entity.ServiceProvider serviceProvider; private Dataspace dataspace; @DynamicPropertySource - static void configureProperties(DynamicPropertyRegistry registry) throws IOException { + static void configureProperties(DynamicPropertyRegistry registry) { registry.add("tenant-manager.url", () -> "http://%s:%s".formatted(mockBackEndHost, mockBackEndPort)); registry.add("vault.url", () -> "http://%s:%s/vault".formatted(mockBackEndHost, mockBackEndPort)); registry.add("dataplane.url", () -> "http://%s:%s/dataplane".formatted(mockBackEndHost, mockBackEndPort)); registry.add("dataplane.internal.url", () -> "http://%s:%s/dataplane".formatted(mockBackEndHost, mockBackEndPort)); registry.add("controlplane.url", () -> "http://%s:%s/controlplane".formatted(mockBackEndHost, mockBackEndPort)); - } @AfterEach @@ -130,7 +129,7 @@ void setUp() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(InetAddress.getByName(mockBackEndHost), mockBackEndPort); - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("test-token"); + when(tokenProvider.getToken(anyString(), anyString())).thenReturn("test-token"); when(webDidResolver.resolveProtocolEndpoints(anyString())).thenReturn("http://example.com/api"); } @@ -426,12 +425,12 @@ void shouldGetParticipantDataspaces() throws Exception { var participant = new Participant(); participant.setIdentifier("Test Participant"); participant.setTenant(tenant); - + // Add dataspace info to participant var dataspaceInfo = new com.metaformsystems.redline.domain.entity.DataspaceInfo(); dataspaceInfo.setDataspaceId(dataspace.getId()); participant.getDataspaceInfos().add(dataspaceInfo); - + tenant.addParticipant(participant); participant = participantRepository.save(participant); @@ -459,17 +458,17 @@ void shouldGetParticipantDataspaces_withMultipleDataspaces() throws Exception { var participant = new Participant(); participant.setIdentifier("Test Participant"); participant.setTenant(tenant); - + // Add first dataspace info var dataspaceInfo1 = new com.metaformsystems.redline.domain.entity.DataspaceInfo(); dataspaceInfo1.setDataspaceId(dataspace.getId()); participant.getDataspaceInfos().add(dataspaceInfo1); - + // Add second dataspace info var dataspaceInfo2 = new com.metaformsystems.redline.domain.entity.DataspaceInfo(); dataspaceInfo2.setDataspaceId(dataspace2.getId()); participant.getDataspaceInfos().add(dataspaceInfo2); - + tenant.addParticipant(participant); participant = participantRepository.save(participant); @@ -492,12 +491,12 @@ void shouldCreatePartnerReference() throws Exception { var participant = new Participant(); participant.setIdentifier("Test Participant"); participant.setTenant(tenant); - + // Add dataspace info to participant var dataspaceInfo = new com.metaformsystems.redline.domain.entity.DataspaceInfo(); dataspaceInfo.setDataspaceId(dataspace.getId()); participant.getDataspaceInfos().add(dataspaceInfo); - + tenant.addParticipant(participant); participant = participantRepository.save(participant); @@ -531,11 +530,11 @@ void shouldCreatePartnerReference_withProperties() throws Exception { var participant = new Participant(); participant.setIdentifier("Test Participant"); participant.setTenant(tenant); - + var dataspaceInfo = new com.metaformsystems.redline.domain.entity.DataspaceInfo(); dataspaceInfo.setDataspaceId(dataspace.getId()); participant.getDataspaceInfos().add(dataspaceInfo); - + tenant.addParticipant(participant); participant = participantRepository.save(participant); diff --git a/src/test/java/com/metaformsystems/redline/application/service/OAuth2TokenProviderTest.java b/src/test/java/com/metaformsystems/redline/application/service/OAuth2TokenProviderTest.java deleted file mode 100644 index 60fba75..0000000 --- a/src/test/java/com/metaformsystems/redline/application/service/OAuth2TokenProviderTest.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright (c) 2026 Metaform Systems, Inc. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package com.metaformsystems.redline.application.service; - -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.test.util.TestSocketUtils; -import org.springframework.web.reactive.function.client.WebClient; - -import java.io.IOException; -import java.net.InetAddress; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@DisplayName("OAuth2TokenProvider Tests") -class OAuth2TokenProviderTest { - - private static final String CLIENT_ID = "test-client"; - private static final String CLIENT_SECRET = "test-secret"; - private static final String SCOPES = "identity-api:read identity-api:write"; - - private MockWebServer mockWebServer; - private OAuth2TokenProvider tokenProvider; - - @BeforeEach - void setUp() throws IOException { - mockWebServer = new MockWebServer(); - mockWebServer.start(InetAddress.getByName("localhost"), TestSocketUtils.findAvailableTcpPort()); - - var webClient = WebClient.builder() - .baseUrl(mockWebServer.url("/").toString()) - .build(); - - tokenProvider = new OAuth2TokenProvider(webClient); - } - - @AfterEach - void tearDown() throws IOException { - if (mockWebServer != null) { - mockWebServer.shutdown(); - } - } - - @Test - @DisplayName("should get access token successfully") - void getToken_success() throws InterruptedException { - var responseBody = """ - { - "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", - "expires_in": 3600, - "refresh_expires_in": 0, - "token_type": "Bearer", - "not-before-policy": 0, - "scope": "identity-api:read identity-api:write" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - - assertNotNull(result); - assertEquals("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", result); - - var recordedRequest = mockWebServer.takeRequest(); - assertNotNull(recordedRequest); - assertEquals("POST", recordedRequest.getMethod()); - assertEquals("/", recordedRequest.getPath()); - - // Verify request body contains all expected parameters - var requestBody = recordedRequest.getBody().readUtf8(); - assertNotNull(requestBody); - assertRequestBodyContains(requestBody, "grant_type=client_credentials"); - assertRequestBodyContains(requestBody, "client_id=" + CLIENT_ID); - assertRequestBodyContains(requestBody, "client_secret=" + CLIENT_SECRET); - assertRequestBodyContains(requestBody, "scope=identity-api:read"); - assertRequestBodyContains(requestBody, "identity-api:write"); - } - - @Test - @DisplayName("should handle token with different scope") - void getToken_differentScope() throws InterruptedException { - var customScopes = "read:only"; - var responseBody = """ - { - "access_token": "different-token-value", - "expires_in": 7200, - "token_type": "Bearer" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, customScopes); - - assertNotNull(result); - assertEquals("different-token-value", result); - - var recordedRequest = mockWebServer.takeRequest(); - var requestBody = recordedRequest.getBody().readUtf8(); - assertRequestBodyContains(requestBody, "scope=" + customScopes); - } - - @Test - @DisplayName("should handle token response with minimal fields") - void getToken_minimalResponse() { - var responseBody = """ - { - "access_token": "minimal-token" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - - assertNotNull(result); - assertEquals("minimal-token", result); - } - - @Test - @DisplayName("should include form content type in request") - void getToken_contentType() throws InterruptedException { - var responseBody = """ - { - "access_token": "test-token" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - - var recordedRequest = mockWebServer.takeRequest(); - var contentType = recordedRequest.getHeader("Content-Type"); - assertNotNull(contentType); - assertEquals("application/x-www-form-urlencoded", contentType); - } - - @Test - @DisplayName("should handle special characters in client credentials") - void getToken_specialCharacters() throws InterruptedException { - var specialClientId = "client@example.com"; - var specialClientSecret = "secret&with#special$chars"; - var responseBody = """ - { - "access_token": "special-char-token" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(specialClientId, specialClientSecret, SCOPES); - - assertNotNull(result); - assertEquals("special-char-token", result); - - var recordedRequest = mockWebServer.takeRequest(); - var requestBody = recordedRequest.getBody().readUtf8(); - assertNotNull(requestBody); - // Verify that the request was properly formed even with special characters - assertRequestBodyContains(requestBody, "grant_type=client_credentials"); - } - - @Test - @DisplayName("should handle empty scope") - void getToken_emptyScope() { - var responseBody = """ - { - "access_token": "token-with-empty-scope" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, ""); - - assertNotNull(result); - assertEquals("token-with-empty-scope", result); - } - - @Test - @DisplayName("should handle long access token") - void getToken_longAccessToken() { - var longToken = "eyJhbGciOiJSUzI1NiIsInR5cC6IkpXVCIsImtpZCI6IjEyMzQ1Njc4OTAifQ." + - "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + - "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - var responseBody = "{\"access_token\": \"" + longToken + "\"}"; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - - assertNotNull(result); - assertEquals(longToken, result); - } - - @Test - @DisplayName("should handle multiple consecutive token requests") - void getToken_multipleRequests() { - var token1 = "first-token"; - var token2 = "second-token"; - var token3 = "third-token"; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody("{\"access_token\": \"" + token1 + "\"}")); - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody("{\"access_token\": \"" + token2 + "\"}")); - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody("{\"access_token\": \"" + token3 + "\"}")); - - var result1 = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - var result2 = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - var result3 = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - - assertEquals(token1, result1); - assertEquals(token2, result2); - assertEquals(token3, result3); - - // Verify all three requests were made - assertEquals(3, mockWebServer.getRequestCount()); - } - - @Test - @DisplayName("should handle response with additional fields") - void getToken_responseWithAdditionalFields() { - var responseBody = """ - { - "access_token": "token-with-extras", - "expires_in": 3600, - "refresh_expires_in": 86400, - "refresh_token": "refresh-token-value", - "token_type": "Bearer", - "id_token": "id-token-value", - "not-before-policy": 0, - "session_state": "session-123", - "scope": "identity-api:read identity-api:write", - "custom_claim": "custom_value" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - var result = tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES); - - assertNotNull(result); - assertEquals("token-with-extras", result); - } - - @Test - @DisplayName("should throw exception on null response") - void getToken_nullResponse() { - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody("null")); - - assertThat(tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES)).isNull(); - } - - @Test - @DisplayName("should handle error response gracefully") - void getToken_errorResponse() { - var errorResponse = """ - { - "error": "invalid_client", - "error_description": "Client authentication failed" - } - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(400) - .addHeader("Content-Type", "application/json") - .setBody(errorResponse)); - - assertThrows(Exception.class, () -> - tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, SCOPES) - ); - } - - // Helper method to assert request body contains a substring - private void assertRequestBodyContains(String requestBody, String expectedSubstring) { - assertNotNull(requestBody, "Request body should not be null"); - assertTrue(requestBody.contains(expectedSubstring), - "Request body should contain '" + expectedSubstring + "', but was: " + requestBody); - } -} diff --git a/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java b/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java index 99e4b42..960efe7 100644 --- a/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java +++ b/src/test/java/com/metaformsystems/redline/domain/service/DataAccessServiceIntegrationTest.java @@ -84,7 +84,7 @@ class DataAccessServiceIntegrationTest { @Autowired private ServiceProviderRepository serviceProviderRepository; private ServiceProvider serviceProvider; - @MockitoBean + @MockitoBean("token-exchange") private TokenProvider tokenProvider; @MockitoBean @@ -119,7 +119,7 @@ void setUp() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(InetAddress.getByName(mockBackEndHost), mockBackEndPort); - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("mock-token"); + when(tokenProvider.getToken(anyString(), anyString())).thenReturn("mock-token"); } @Test diff --git a/src/test/java/com/metaformsystems/redline/domain/service/TenantServiceIntegrationTest.java b/src/test/java/com/metaformsystems/redline/domain/service/TenantServiceIntegrationTest.java index f9b561d..5942cd9 100644 --- a/src/test/java/com/metaformsystems/redline/domain/service/TenantServiceIntegrationTest.java +++ b/src/test/java/com/metaformsystems/redline/domain/service/TenantServiceIntegrationTest.java @@ -85,7 +85,7 @@ class TenantServiceIntegrationTest { private ServiceProviderRepository serviceProviderRepository; private ServiceProvider serviceProvider; private Dataspace dataspace; - @MockitoBean + @MockitoBean("token-exchange") private TokenProvider tokenProvider; @MockitoBean @@ -118,7 +118,7 @@ void setUp() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(InetAddress.getByName(mockBackEndHost), mockBackEndPort); - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("mock-token"); + when(tokenProvider.getToken(anyString(), anyString())).thenReturn("mock-token"); } @Test diff --git a/src/test/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientIntegrationTest.java b/src/test/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientIntegrationTest.java index 7f745f7..4122db9 100644 --- a/src/test/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientIntegrationTest.java +++ b/src/test/java/com/metaformsystems/redline/infrastructure/client/dataplane/DataPlaneApiClientIntegrationTest.java @@ -51,7 +51,7 @@ class DataPlaneApiClientIntegrationTest { @Autowired private ParticipantRepository participantRepository; - @MockitoBean + @MockitoBean("token-exchange") private TokenProvider tokenProvider; private String participantContextId; @@ -81,7 +81,7 @@ void setUp() { participantRepository.save(participant); // Mock token provider to return a test token - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("test-token"); + when(tokenProvider.getToken(anyString(), anyString())).thenReturn("test-token"); } @Test diff --git a/src/test/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImplTest.java b/src/test/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImplTest.java index aae07d0..46d1556 100644 --- a/src/test/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImplTest.java +++ b/src/test/java/com/metaformsystems/redline/infrastructure/client/identityhub/IdentityHubClientImplTest.java @@ -44,6 +44,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -80,9 +82,9 @@ void setUp() throws IOException { ADMIN_CLIENT_SECRET ); - when(tokenProvider.getToken(ADMIN_CLIENT_ID, ADMIN_CLIENT_SECRET, "identity-api:read")) + when(tokenProvider.getToken(anyString(), eq("identity-api:read"))) .thenReturn(TEST_TOKEN); - when(tokenProvider.getToken(CLIENT_ID, CLIENT_SECRET, "identity-api:write identity-api:read")) + when(tokenProvider.getToken(anyString(), eq("identity-api:write identity-api:read"))) .thenReturn(TEST_TOKEN); } @@ -93,58 +95,6 @@ void tearDown() throws IOException { } } - @Test - @DisplayName("should get all participants successfully") - void getAllParticipants_success() { - - var responseBody = """ - [ - { - "id": "p1", - "participantContextId": "context-1", - "did": "did:example:123", - "apiTokenAlias": "token-alias", - "roles": ["role1"], - "properties": {}, - "state": 1, - "createdAt": 1234567890, - "lastModified": 1234567890 - } - ] - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - - var result = identityHubClient.getAllParticipants(); - - - assertNotNull(result); - assertEquals(1, result.size()); - assertEquals("context-1", result.getFirst().participantContextId()); - } - - @Test - @DisplayName("should return empty list when no participants exist") - void getAllParticipants_empty() { - - var responseBody = "[]"; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - - var result = identityHubClient.getAllParticipants(); - - assertNotNull(result); - assertEquals(0, result.size()); - } - @Test @DisplayName("should get participant by context id successfully") @@ -195,52 +145,6 @@ void getParticipant_notFound() { }); } - @Test - @DisplayName("should get all credentials successfully") - void getAllCredentials_success() { - - var responseBody = """ - [ - { - "id": "cred-1", - "credential": "eyJhbGciOiJFZERTQSJ9..." - } - ] - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - - var result = identityHubClient.getAllCredentials(); - - - assertNotNull(result); - assertEquals(1, result.size()); - } - - @Test - @DisplayName("should return empty list when no credentials exist") - void getAllCredentials_empty() { - - var responseBody = "[]"; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - - var result = identityHubClient.getAllCredentials(); - - - assertNotNull(result); - assertEquals(0, result.size()); - } - - @Test @DisplayName("should query credentials by type successfully") void queryCredentialsByType_success() { @@ -353,34 +257,6 @@ void requestCredential_success() throws InterruptedException { assertEquals("POST", recordedRequest.getMethod()); } - - @Test - @DisplayName("should get all key pairs successfully") - void getAllKeyPairs_success() { - - var responseBody = """ - [ - { - "keyId": "key-1", - "publicKeyPem": "-----BEGIN PUBLIC KEY-----..." - } - ] - """; - - mockWebServer.enqueue(new MockResponse() - .setResponseCode(200) - .addHeader("Content-Type", "application/json") - .setBody(responseBody)); - - - var result = identityHubClient.getAllKeyPairs(); - - - assertNotNull(result); - assertEquals(1, result.size()); - } - - @Test @DisplayName("should query key pairs by participant context id successfully") void queryKeyPairByParticipantContextId_success() { diff --git a/src/test/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientIntegrationTest.java b/src/test/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientIntegrationTest.java index 91578dd..061353a 100644 --- a/src/test/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientIntegrationTest.java +++ b/src/test/java/com/metaformsystems/redline/infrastructure/client/management/ManagementApiClientIntegrationTest.java @@ -51,6 +51,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.when; @SpringBootTest @@ -67,7 +69,7 @@ class ManagementApiClientIntegrationTest { @Autowired private ParticipantRepository participantRepository; - @MockitoBean + @MockitoBean("token-exchange") private TokenProvider tokenProvider; private Participant participant; @@ -96,10 +98,10 @@ void setUp() throws IOException { mockWebServer = new MockWebServer(); mockWebServer.start(InetAddress.getByName(mockBackEndHost), mockBackEndPort); - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("mock-token"); + when(tokenProvider.getToken(eq(participantContextId), anyString())).thenReturn("mock-token"); // Mock token provider to return a test token - when(tokenProvider.getToken(anyString(), anyString(), anyString())).thenReturn("test-token"); + when(tokenProvider.getToken(eq(participantContextId), anyString())).thenReturn("test-token"); } @Test @@ -428,6 +430,8 @@ void shouldCreateCelExpression() throws InterruptedException { .scopes(Set.of("scope1", "scope2")) .build(); + when(tokenProvider.getToken(isNull(), anyString())).thenReturn("test-token"); + // Mock CEL expression creation response mockWebServer.enqueue(new MockResponse() .setResponseCode(204) diff --git a/src/test/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/TenantManagerClientImplTest.java b/src/test/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/TenantManagerClientImplTest.java index c038720..a1fceb5 100644 --- a/src/test/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/TenantManagerClientImplTest.java +++ b/src/test/java/com/metaformsystems/redline/infrastructure/client/tenantmanager/TenantManagerClientImplTest.java @@ -37,6 +37,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -57,10 +59,10 @@ void setUp() throws IOException { .baseUrl(mockWebServer.url("/").toString()) .build(); - when(tokenProvider.getToken("provisioner", "provisioner-secret", "identity-api:read")) + when(tokenProvider.getToken(anyString(), eq("identity-api:read"))) .thenReturn(TEST_TOKEN); - tenantManagerClient = new TenantManagerClientImpl(webClient, tokenProvider, "provisioner", "provisioner-secret"); + tenantManagerClient = new TenantManagerClientImpl(webClient, tokenProvider); } @AfterEach From d3c17d21a9d334fdcece539a7b9595cd7fab887d Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Mon, 15 Jun 2026 15:01:04 +0200 Subject: [PATCH 2/2] move url to api invocation --- Dockerfile | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../redline/application/service/WorkloadTokenProvider.java | 1 + .../redline/infrastructure/config/TokenExchangeConfig.java | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6ac8e9d..4285d7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM gradle:8.5-jdk21 AS build +FROM gradle:9.5-jdk21 AS build WORKDIR /app # Copy gradle files for dependency caching diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4352e16..55c1cec 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -13,7 +13,7 @@ # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java b/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java index b0e6ade..3bf4e76 100644 --- a/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java +++ b/src/main/java/com/metaformsystems/redline/application/service/WorkloadTokenProvider.java @@ -56,6 +56,7 @@ public String getToken(String resource, String scopes) { var tokenContent = Files.readString(Path.of(tokenFilePath)); var response = webClient.post() + .uri("/token") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(BodyInserters.fromFormData("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") .with("subject_token", tokenContent) diff --git a/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java b/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java index 34bda2c..f815aee 100644 --- a/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java +++ b/src/main/java/com/metaformsystems/redline/infrastructure/config/TokenExchangeConfig.java @@ -21,7 +21,7 @@ @Configuration public class TokenExchangeConfig { - @Value("${token.exchange.url:http://localhost:8080/token}") + @Value("${token.exchange.url:http://localhost:8080}") private String tokenExchangeUrl; @Bean