From 8a17aec6ddf330d730df0e9262c88abc2e297e0b Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Wed, 17 Jul 2024 18:19:50 +0530 Subject: [PATCH 01/60] feat: added new fields in did document --- .../constant/StringPool.java | 8 ++++++ .../service/JwtPresentationES256KService.java | 19 +++++++++++++- .../wallet/WalletTest.java | 26 ++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java index dc137b8b..83768e6a 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java @@ -94,4 +94,12 @@ private StringPool() { public static final String AS_JWT = "asJwt"; public static final String BPN_CREDENTIAL = "BpnCredential"; + + public static final String ASSERTION_METHOD = "assertionMethod"; + public static final String CONTEXT = "@context"; + public static final String SERVICE_ENDPOINT = "serviceEndpoint"; + public static final String SERVICE = "service"; + public static final String SECURITY_TOKEN_SERVICE = "SecurityTokenService"; + public static final String CREDENTIAL_SERVICE = "CredentialService"; + public static final String HTTPS_SCHEME = "https://"; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java index 1d057d74..71d02d40 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java @@ -36,6 +36,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.exception.SignatureFailureException; import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedAlgorithmException; @@ -44,6 +45,7 @@ import org.eclipse.tractusx.ssi.lib.model.did.DidDocumentBuilder; import org.eclipse.tractusx.ssi.lib.model.did.JWKVerificationMethod; import org.eclipse.tractusx.ssi.lib.model.did.VerificationMethod; +import org.eclipse.tractusx.ssi.lib.model.verifiable.Verifiable; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentationBuilder; @@ -130,7 +132,22 @@ public DidDocument buildDidDocument(String bpn, Did did, List ids = new ArrayList<>(); + jwkVerificationMethods.forEach((verificationMethod) -> { + ids.add(verificationMethod.getId()); + }); + didDocument.put(StringPool.ASSERTION_METHOD, ids); + //add service + Map serviceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.SECURITY_TOKEN_SERVICE, Verifiable.TYPE, StringPool.SECURITY_TOKEN_SERVICE, + StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host() + "/api/token"); + org.eclipse.tractusx.ssi.lib.model.did.Service service1 = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData); + Map serviceData2 = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.CREDENTIAL_SERVICE, Verifiable.TYPE, StringPool.CREDENTIAL_SERVICE, + StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host()); + org.eclipse.tractusx.ssi.lib.model.did.Service service2 = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData2); + didDocument.put(StringPool.SERVICE, List.of(service1,service2)); + didDocument = DidDocument.fromJson(didDocument.toJson()); log.debug("did document created for bpn ->{}", StringEscapeUtils.escapeJava(bpn)); return didDocument; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java index 8371a0fe..7095a256 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java @@ -23,10 +23,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.jwk.Curve; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -40,6 +42,8 @@ import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; import org.eclipse.tractusx.ssi.lib.did.web.DidWebFactory; +import org.eclipse.tractusx.ssi.lib.model.did.JWKVerificationMethod; +import org.eclipse.tractusx.ssi.lib.model.did.VerificationMethod; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -60,6 +64,8 @@ import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -168,7 +174,25 @@ void createWalletTest201() throws JsonProcessingException, JSONException { Assertions.assertNotNull(response.getBody()); Assertions.assertNotNull(wallet.getDidDocument()); - Assertions.assertEquals(2, wallet.getDidDocument().getVerificationMethods().size()); + List verificationMethods = wallet.getDidDocument().getVerificationMethods(); + Assertions.assertEquals(2, verificationMethods.size()); + + // both public keys will include the publicKeyJwk format to express the public key + List curves = verificationMethods.stream().map(vm -> (LinkedHashMap) vm.get(JWKVerificationMethod.PUBLIC_KEY_JWK)) + .map(lhm -> lhm.get(JWKVerificationMethod.JWK_CURVE).toString()).toList(); + List algorithms = Arrays.asList(Curve.SECP256K1.toString(),Curve.Ed25519.toString()); + // both the Ed25519 and the secp256k1 curve keys must be present in the verificationMethod of a did document + Assertions.assertTrue(curves.containsAll(algorithms)); + List assertionMethod = (List)wallet.getDidDocument().get(StringPool.ASSERTION_METHOD); + // both public keys must be expressed in the assertionMethod + Assertions.assertEquals(2, assertionMethod.size()); + // both public keys will use the JsonWebKey2020 verification method type + Assertions.assertTrue(verificationMethods.get(0).getType().equals(JWKVerificationMethod.DEFAULT_TYPE) && + verificationMethods.get(1).getType().equals(JWKVerificationMethod.DEFAULT_TYPE)); + // the controller for the keys is the MIW + Assertions.assertEquals(verificationMethods.get(0).getController().toString(), wallet.getDid()); + Assertions.assertEquals(verificationMethods.get(1).getController().toString(), wallet.getDid()); + List context = wallet.getDidDocument().getContext(); miwSettings.didDocumentContextUrls().forEach(uri -> { Assertions.assertTrue(context.contains(uri)); From 5961854ad811615453ea2afff15c1e4955ca450d Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Thu, 18 Jul 2024 10:07:06 +0530 Subject: [PATCH 02/60] fix: updated code as per review --- .../managedidentitywallets/constant/StringPool.java | 1 - .../service/JwtPresentationES256KService.java | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java index 83768e6a..c14c7530 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java @@ -96,7 +96,6 @@ private StringPool() { public static final String BPN_CREDENTIAL = "BpnCredential"; public static final String ASSERTION_METHOD = "assertionMethod"; - public static final String CONTEXT = "@context"; public static final String SERVICE_ENDPOINT = "serviceEndpoint"; public static final String SERVICE = "service"; public static final String SECURITY_TOKEN_SERVICE = "SecurityTokenService"; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java index 71d02d40..a82efae0 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java @@ -40,6 +40,7 @@ import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.exception.SignatureFailureException; import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedAlgorithmException; +import org.eclipse.tractusx.ssi.lib.model.JsonLdObject; import org.eclipse.tractusx.ssi.lib.model.did.Did; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.did.DidDocumentBuilder; @@ -132,7 +133,7 @@ public DidDocument buildDidDocument(String bpn, Did did, List ids = new ArrayList<>(); jwkVerificationMethods.forEach((verificationMethod) -> { @@ -142,11 +143,11 @@ public DidDocument buildDidDocument(String bpn, Did did, List serviceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.SECURITY_TOKEN_SERVICE, Verifiable.TYPE, StringPool.SECURITY_TOKEN_SERVICE, StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host() + "/api/token"); - org.eclipse.tractusx.ssi.lib.model.did.Service service1 = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData); + org.eclipse.tractusx.ssi.lib.model.did.Service tokenService = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData); Map serviceData2 = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.CREDENTIAL_SERVICE, Verifiable.TYPE, StringPool.CREDENTIAL_SERVICE, StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host()); - org.eclipse.tractusx.ssi.lib.model.did.Service service2 = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData2); - didDocument.put(StringPool.SERVICE, List.of(service1,service2)); + org.eclipse.tractusx.ssi.lib.model.did.Service credentialService = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData2); + didDocument.put(StringPool.SERVICE, List.of(tokenService,credentialService)); didDocument = DidDocument.fromJson(didDocument.toJson()); log.debug("did document created for bpn ->{}", StringEscapeUtils.escapeJava(bpn)); From 44af0670f1e87b5ebedf88f66adcde89157d08fa Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Thu, 18 Jul 2024 10:14:05 +0530 Subject: [PATCH 03/60] fix: updated var name --- .../service/JwtPresentationES256KService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java index a82efae0..084e06b1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java @@ -141,12 +141,12 @@ public DidDocument buildDidDocument(String bpn, Did did, List serviceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.SECURITY_TOKEN_SERVICE, Verifiable.TYPE, StringPool.SECURITY_TOKEN_SERVICE, + Map tokenServiceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.SECURITY_TOKEN_SERVICE, Verifiable.TYPE, StringPool.SECURITY_TOKEN_SERVICE, StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host() + "/api/token"); - org.eclipse.tractusx.ssi.lib.model.did.Service tokenService = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData); - Map serviceData2 = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.CREDENTIAL_SERVICE, Verifiable.TYPE, StringPool.CREDENTIAL_SERVICE, + org.eclipse.tractusx.ssi.lib.model.did.Service tokenService = new org.eclipse.tractusx.ssi.lib.model.did.Service(tokenServiceData); + Map credentialServiceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.CREDENTIAL_SERVICE, Verifiable.TYPE, StringPool.CREDENTIAL_SERVICE, StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host()); - org.eclipse.tractusx.ssi.lib.model.did.Service credentialService = new org.eclipse.tractusx.ssi.lib.model.did.Service(serviceData2); + org.eclipse.tractusx.ssi.lib.model.did.Service credentialService = new org.eclipse.tractusx.ssi.lib.model.did.Service(credentialServiceData); didDocument.put(StringPool.SERVICE, List.of(tokenService,credentialService)); didDocument = DidDocument.fromJson(didDocument.toJson()); From 9b3381de3b31d2c8eea660f6d5493b1f9e064f46 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Jul 2024 13:27:53 +0000 Subject: [PATCH 04/60] chore(release): 0.6.0-develop.1 [skip ci] # [0.6.0-develop.1](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.5.0...v0.6.0-develop.1) (2024-07-18) ### Bug Fixes * updated code as per review ([5961854](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/5961854ad811615453ea2afff15c1e4955ca450d)) * updated var name ([44af067](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/44af0670f1e87b5ebedf88f66adcde89157d08fa)) ### Features * added new fields in did document ([8a17aec](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/8a17aec6ddf330d730df0e9262c88abc2e297e0b)) --- CHANGELOG.md | 13 +++++++++++++ charts/managed-identity-wallet/Chart.yaml | 4 ++-- charts/managed-identity-wallet/README.md | 2 +- gradle.properties | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d65289a..4769e864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [0.6.0-develop.1](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.5.0...v0.6.0-develop.1) (2024-07-18) + + +### Bug Fixes + +* updated code as per review ([5961854](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/5961854ad811615453ea2afff15c1e4955ca450d)) +* updated var name ([44af067](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/44af0670f1e87b5ebedf88f66adcde89157d08fa)) + + +### Features + +* added new fields in did document ([8a17aec](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/8a17aec6ddf330d730df0e9262c88abc2e297e0b)) + # [0.5.0](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.4.0...v0.5.0) (2024-07-05) diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 9d7913b2..6fafb4c5 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -25,8 +25,8 @@ description: | type: application -version: 0.5.0 -appVersion: 0.5.0 +version: 0.6.0-develop.1 +appVersion: 0.6.0-develop.1 home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 6983f7f2..3c750d75 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -2,7 +2,7 @@ # managed-identity-wallet -![Version: 0.5.0](https://img.shields.io/badge/Version-0.5.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.5.0](https://img.shields.io/badge/AppVersion-0.5.0-informational?style=flat-square) +![Version: 0.6.0-develop.1](https://img.shields.io/badge/Version-0.6.0--develop.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.6.0-develop.1](https://img.shields.io/badge/AppVersion-0.6.0--develop.1-informational?style=flat-square) Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. diff --git a/gradle.properties b/gradle.properties index 469782dd..1ab8b3d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ jacocoVersion=0.8.9 springBootVersion=3.1.6 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=0.5.0 +applicationVersion=0.6.0-develop.1 openApiVersion=2.1.0 From e3c5450fe2e0084f9deee16dff59e228afa40966 Mon Sep 17 00:00:00 2001 From: Dominik Pinsel Date: Fri, 12 Jul 2024 14:34:25 +0200 Subject: [PATCH 05/60] feat(identity-trust)!: update IATP protocol BREAKING CHANGE: `/api/presentations/iatp` endpoint now accepts PresentationQueryMessage and returns PresentationResponseMessage objects. Signed-off-by: Dominik Pinsel --- .../security/PresentationIatpFilter.java | 5 +- .../config/security/SecurityConfig.java | 5 +- .../constant/RestURI.java | 6 + .../controller/PresentationController.java | 38 ++++- .../dto/PresentationResponseMessage.java | 58 +++++++ .../reader/TractusXJsonLdReader.java | 79 +++++++++ .../TractusXPresentationRequestReader.java | 85 +++++++++ .../utils/ResourceUtil.java | 61 +++++++ .../resources/jsonld/IdentityMinusTrust.json | 127 ++++++++++++++ ...n.presentation-exchange.submission.v1.json | 15 ++ ...PresentationResponseSerializationTest.java | 112 ++++++++++++ .../identityminustrust/TokenRequestTest.java | 161 ++++++++++++++++++ .../reader/PresentationRequestReaderTest.java | 49 ++++++ .../messages/presentation_query.json | 10 ++ 14 files changed, 798 insertions(+), 13 deletions(-) create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXJsonLdReader.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXPresentationRequestReader.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/ResourceUtil.java create mode 100644 miw/src/main/resources/jsonld/IdentityMinusTrust.json create mode 100644 miw/src/main/resources/jsonld/identity.foundation.presentation-exchange.submission.v1.json create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseSerializationTest.java create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/reader/PresentationRequestReaderTest.java create mode 100644 miw/src/test/resources/identityminustrust/messages/presentation_query.json diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java index 61ca5acd..cefefb8f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java @@ -43,7 +43,8 @@ public class PresentationIatpFilter extends GenericFilterBean { - RequestMatcher customFilterUrl = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP); + RequestMatcher customFilterUrl1 = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP); + RequestMatcher customFilterUrl2 = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP_WORKAROUND); STSTokenValidationService validationService; @@ -57,7 +58,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; - if (customFilterUrl.matches(httpServletRequest)) { + if (customFilterUrl1.matches(httpServletRequest) || customFilterUrl2.matches(httpServletRequest)) { String authHeader = httpServletRequest.getHeader("Authorization"); if (StringUtils.isEmpty(authHeader)) { httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index 59bce9fa..148f1875 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -81,7 +81,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(new AntPathRequestMatcher("/ui/swagger-ui/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/actuator/health/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/api/token", POST.name())).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/presentations/iatp", GET.name())).permitAll() + .requestMatchers(new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP, POST.name())).permitAll() + .requestMatchers(new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP_WORKAROUND, POST.name())).permitAll() .requestMatchers(new AntPathRequestMatcher("/actuator/loggers/**")).hasRole(ApplicationRole.ROLE_MANAGE_APP) //did document resolve APIs @@ -137,7 +138,7 @@ public WebSecurityCustomizer securityCustomizer() { */ @Bean public AuthenticationEventPublisher authenticationEventPublisher - (ApplicationEventPublisher applicationEventPublisher) { + (ApplicationEventPublisher applicationEventPublisher) { return new DefaultAuthenticationEventPublisher(applicationEventPublisher); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java index 764a0af4..97e73c97 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java @@ -80,4 +80,10 @@ private RestURI() { */ public static final String API_PRESENTATIONS_IATP = "/api/presentations/iatp"; + /** + * The constant API_PRESENTATIONS_IATP_WORKAROUND. THe EDC assumes (hard coded) that the presentation query endpoint is at /presentations/query. + * To mitigate this issue the MIW has to provide the same endpoint (without documentation), besides the correct one. + */ + public static final String API_PRESENTATIONS_IATP_WORKAROUND = "/presentations/query"; + } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index 1a2f7d87..3f5b5a0c 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -24,23 +24,28 @@ import com.nimbusds.jwt.SignedJWT; import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.GetVerifiablePresentationIATPApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationValidationApiDocs; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.dto.PresentationResponseMessage; +import org.eclipse.tractusx.managedidentitywallets.reader.TractusXPresentationRequestReader; import org.eclipse.tractusx.managedidentitywallets.service.PresentationService; +import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.io.InputStream; import java.security.Principal; +import java.util.List; import java.util.Map; import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getAccessToken; @@ -55,6 +60,8 @@ public class PresentationController extends BaseController { private final PresentationService presentationService; + private final TractusXPresentationRequestReader presentationRequestReader; + /** * Create presentation response entity. * @@ -97,17 +104,30 @@ public ResponseEntity> validatePresentation(@RequestBody Map /** * Create presentation response entity for VC types provided in STS token. * - * @param stsToken the STS token with required scopes - * @param asJwt as JWT VP response + * @param stsToken the STS token with required scopes + * @param asJwt as JWT VP response * @return the VP response entity */ - @GetMapping(path = RestURI.API_PRESENTATIONS_IATP, produces = { MediaType.APPLICATION_JSON_VALUE }) + + @PostMapping(path = { RestURI.API_PRESENTATIONS_IATP, RestURI.API_PRESENTATIONS_IATP_WORKAROUND }, produces = { MediaType.APPLICATION_JSON_VALUE }) @GetVerifiablePresentationIATPApiDocs - public ResponseEntity> createPresentation(@Parameter(hidden = true) @RequestHeader(name = "Authorization") String stsToken, - @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt) { - SignedJWT accessToken = getAccessToken(stsToken); - Map vp = presentationService.createVpWithRequiredScopes(accessToken, asJwt); - return ResponseEntity.ok(vp); + @SneakyThrows + public ResponseEntity createPresentation(@Parameter(hidden = true) @RequestHeader(name = "Authorization") String stsToken, + @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, + InputStream is) { + try { + + final List requestedScopes = presentationRequestReader.readVerifiableCredentialScopes(is); + // requested scopes are ignored until the documentation is better refined + + SignedJWT accessToken = getAccessToken(stsToken); + Map map = presentationService.createVpWithRequiredScopes(accessToken, asJwt); + VerifiablePresentation verifiablePresentation = new VerifiablePresentation((Map)map.get("vp")); + PresentationResponseMessage message = new PresentationResponseMessage(verifiablePresentation); + return ResponseEntity.ok(message); + } catch (TractusXPresentationRequestReader.InvalidPresentationQueryMessageResource e) { + return ResponseEntity.badRequest().build(); + } } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java new file mode 100644 index 00000000..5636f3cf --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java @@ -0,0 +1,58 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; + +import java.util.List; + +/** + * Class to represent the response message of a presentation request. + * Defined in JsonLD Tractus-X context.json. + *

+ * As `presentationSubmission` a not well-defined, we will just skip the property for HTTP responses. Defining all types as 'Json' make the whole idea of using Json-Linked-Data a waste of time, but ok. + *

+ * The `presentation` property is only specified as 'Json'. For this implementation we will assume these are Presentations from ether the Verifiable Credential Data Model v1.1 or Verifiable Credential Data Model v2.0. + */ +@Getter +public class PresentationResponseMessage { + + + public PresentationResponseMessage(VerifiablePresentation verifiablePresentation) { + this(List.of(verifiablePresentation)); + } + + public PresentationResponseMessage(List verifiablePresentations) { + this.verifiablePresentations = verifiablePresentations; + } + + @JsonProperty("@context") + private List contexts = List.of("https://w3id.org/tractusx-trust/v0.8"); + + @JsonProperty("@type") + private List types = List.of("PresentationResponseMessage"); + + @JsonProperty("presentation") + private List verifiablePresentations; +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXJsonLdReader.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXJsonLdReader.java new file mode 100644 index 00000000..075c046a --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXJsonLdReader.java @@ -0,0 +1,79 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.reader; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.JsonLdOptions; +import com.apicatalog.jsonld.document.JsonDocument; +import com.apicatalog.jsonld.processor.ExpansionProcessor; +import jakarta.json.JsonArray; +import lombok.NonNull; +import org.eclipse.tractusx.managedidentitywallets.utils.ResourceUtil; +import org.eclipse.tractusx.ssi.lib.model.RemoteDocumentLoader; + +import java.io.InputStream; +import java.net.URI; +import java.util.Map; + +public class TractusXJsonLdReader { + + private static final String TRACTUS_X_CONTEXT_RESOURCE = "jsonld/IdentityMinusTrust.json"; + private static final URI TRACTUS_X_CONTEXT = URI.create("https://w3id.org/tractusx-trust/v0.8"); + private static final URI IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_CONTEXT = URI.create("https://identity.foundation/presentation-exchange/submission/v1"); + private static final String IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_RESOURCE = "jsonld/identity.foundation.presentation-exchange.submission.v1.json"; + + + private final RemoteDocumentLoader documentLoader = RemoteDocumentLoader.DOCUMENT_LOADER; + + public TractusXJsonLdReader() { + + documentLoader.setEnableLocalCache(true); + + if (!documentLoader.getLocalCache().containsKey(TRACTUS_X_CONTEXT)) { + cacheOfflineResource(TRACTUS_X_CONTEXT_RESOURCE, TRACTUS_X_CONTEXT); + } + if (!documentLoader.getLocalCache().containsKey(IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_CONTEXT)) { + cacheOfflineResource(IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_RESOURCE, IDENTITY_FOUNDATION_CREDENTIAL_SUBMISSION_CONTEXT); + } + } + + public JsonArray expand(@NonNull final InputStream documentStream) throws JsonLdError { + + final JsonLdOptions jsonLdOptions = new JsonLdOptions(); + jsonLdOptions.setDocumentLoader(documentLoader); + + final JsonDocument document = JsonDocument.of(com.apicatalog.jsonld.http.media.MediaType.JSON_LD, documentStream); + return ExpansionProcessor.expand(document, jsonLdOptions, false); + } + + private void cacheOfflineResource(final String resource, final URI context) { + try { + final InputStream resourceStream = ResourceUtil.getResourceStream(resource); + final JsonDocument identityMinusTrustDocument; + identityMinusTrustDocument = JsonDocument.of(com.apicatalog.jsonld.http.media.MediaType.JSON_LD, resourceStream); + documentLoader.getLocalCache().put(context, identityMinusTrustDocument); + } catch (JsonLdError e) { + // If this ever fails, it is a programming error. Loading of the embedded context resource is checked by Unit Tests. + throw new RuntimeException("Could not parse Tractus-X JsonL-d context from resource. This should never happen. Resource: '%s'".formatted(resource), e); + } + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXPresentationRequestReader.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXPresentationRequestReader.java new file mode 100644 index 00000000..502bdeb3 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/reader/TractusXPresentationRequestReader.java @@ -0,0 +1,85 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.reader; + +import com.apicatalog.jsonld.JsonLdError; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.List; + +@Slf4j +@Component +public class TractusXPresentationRequestReader extends TractusXJsonLdReader { + + private static final String JSON_LD_TYPE = "@type"; + private static final String JSON_LD_VALUE = "@value"; + private static final String TRACTUS_X_PRESENTATION_QUERY_MESSAGE_TYPE = "https://w3id.org/tractusx-trust/v0.8/PresentationQueryMessage"; + private static final String TRACTUS_X_SCOPE_TYPE = "https://w3id.org/tractusx-trust/v0.8/scope"; + + public List readVerifiableCredentialScopes(InputStream is) throws InvalidPresentationQueryMessageResource { + try { + + final JsonArray jsonArray = expand(is); + + if (jsonArray.size() != 1) { + log.atDebug().addArgument(jsonArray::toString).log("Expanded JSON-LD: {}"); + throw new InvalidPresentationQueryMessageResource("Expected a single JSON object. Found %d".formatted(jsonArray.size())); + } + + var jsonObject = jsonArray.getJsonObject(0); + + final JsonArray typeArray = jsonObject.getJsonArray(JSON_LD_TYPE); + final List types = typeArray.getValuesAs(JsonString.class).stream().map(JsonString::getString).toList(); + if (!types.contains(TRACTUS_X_PRESENTATION_QUERY_MESSAGE_TYPE)) { + log.atDebug().addArgument(jsonArray::toString).log("Expanded JSON-LD: {}"); + throw new InvalidPresentationQueryMessageResource("Unexpected type. Expected %s".formatted(TRACTUS_X_PRESENTATION_QUERY_MESSAGE_TYPE)); + } + + final JsonArray scopes = jsonObject.getJsonArray(TRACTUS_X_SCOPE_TYPE); + return scopes.getValuesAs(JsonObject.class) + .stream() + .map(o -> o.getJsonString(JSON_LD_VALUE)) + .map(JsonString::getString) + .toList(); + + } catch (JsonLdError e) { + throw new InvalidPresentationQueryMessageResource(e); + } + } + + public static class InvalidPresentationQueryMessageResource extends Exception { + public InvalidPresentationQueryMessageResource(String message) { + super(message); + } + + public InvalidPresentationQueryMessageResource(Throwable cause) { + super(cause); + } + } + +} + diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/ResourceUtil.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/ResourceUtil.java new file mode 100644 index 00000000..6bbd6fd8 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/ResourceUtil.java @@ -0,0 +1,61 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.utils; + +import lombok.SneakyThrows; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +public final class ResourceUtil { + + @SneakyThrows + public static InputStream getResourceStream(String resourceName) { + var stream= ResourceUtil.class.getClassLoader().getResourceAsStream(resourceName); + if(stream == null) { + throw new IllegalArgumentException("File not found"); + } + + return stream; + } + + @SneakyThrows + public static String loadResource(String resourceName) { + StringBuilder content = new StringBuilder(); + + // load resource and return it + try (final InputStream is = ResourceUtil.class.getClassLoader().getResourceAsStream(resourceName)) { + if (is == null) { + throw new IllegalArgumentException("File not found"); + } + + final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append(System.lineSeparator()); + } + } + + return content.toString(); + } +} diff --git a/miw/src/main/resources/jsonld/IdentityMinusTrust.json b/miw/src/main/resources/jsonld/IdentityMinusTrust.json new file mode 100644 index 00000000..4a1fa6fb --- /dev/null +++ b/miw/src/main/resources/jsonld/IdentityMinusTrust.json @@ -0,0 +1,127 @@ +{ + "@context" : { + "@version" : 1.1, + "@protected" : true, + "iatp" : "https://w3id.org/tractusx-trust/v0.8/", + "cred" : "https://www.w3.org/2018/credentials/", + "xsd" : "http://www.w3.org/2001/XMLSchema/", + "CredentialContainer" : { + "@id" : "iatp:CredentialContainer", + "@context" : { + "payload" : { + "@id" : "iatp:payload", + "@type" : "xsd:string" + } + } + }, + "CredentialMessage" : { + "@id" : "iatp:CredentialMessage", + "@context" : { + "credentials" : "iatp:credentials" + } + }, + "CredentialObject" : { + "@id" : "iatp:CredentialObject", + "@context" : { + "credentialType" : { + "@id" : "iatp:credentialType", + "@container" : "@set" + }, + "format" : "iatp:format", + "offerReason" : { + "@id" : "iatp:offerReason", + "@type" : "xsd:string" + }, + "bindingMethods" : { + "@id" : "iatp:bindingMethods", + "@type" : "xsd:string", + "@container" : "@set" + }, + "cryptographicSuites" : { + "@id" : "iatp:cryptographicSuites", + "@type" : "xsd:string", + "@container" : "@set" + }, + "issuancePolicy" : "iatp:issuancePolicy" + } + }, + "CredentialOfferMessage" : { + "@id" : "iatp:CredentialOfferMessage", + "@context" : { + "credentialIssuer" : "cred:issuer", + "credentials" : "iatp:credentials" + } + }, + "CredentialRequestMessage" : { + "@id" : "iatp:CredentialRequestMessage", + "@context" : { + "format" : "iatp:format", + "type" : "@type" + } + }, + "CredentialService" : "iatp:CredentialService", + "CredentialStatus" : { + "@id" : "iatp:CredentialStatus", + "@context" : { + "requestId" : { + "@id" : "iatp:requestId", + "@type" : "@id" + }, + "status" : { + "@id" : "iatp:status", + "@type" : "xsd:string" + } + } + }, + "IssuerMetadata" : { + "@id" : "iatp:IssuerMetadata", + "@context" : { + "credentialIssuer" : "cred:issuer", + "credentialsSupported" : { + "@id" : "iatp:credentialsSupported", + "@container" : "@set" + } + } + }, + "PresentationQueryMessage" : { + "@id" : "iatp:PresentationQueryMessage", + "@context" : { + "presentationDefinition" : { + "@id" : "iatp:presentationDefinition", + "@type" : "@json" + }, + "scope" : { + "@id" : "iatp:scope", + "@type" : "xsd:string", + "@container" : "@set" + } + } + }, + "PresentationResponseMessage" : { + "@id" : "iatp:PresentationResponseMessage", + "@context" : { + "presentation" : { + "@id" : "iatp:presentation", + "@type" : "@json" + }, + "presentationSubmission" : { + "@id" : "iatp:presentationSubmission", + "@type" : "@json" + } + } + }, + "credentials" : { + "@id" : "iatp:credentials", + "@container" : "@set" + }, + "credentialSubject" : { + "@id" : "iatp:credentialSubject", + "@type" : "cred:credentialSubject" + }, + "format" : { + "@id" : "iatp:format", + "@type" : "xsd:string" + }, + "type" : "@type" + } +} diff --git a/miw/src/main/resources/jsonld/identity.foundation.presentation-exchange.submission.v1.json b/miw/src/main/resources/jsonld/identity.foundation.presentation-exchange.submission.v1.json new file mode 100644 index 00000000..49653ceb --- /dev/null +++ b/miw/src/main/resources/jsonld/identity.foundation.presentation-exchange.submission.v1.json @@ -0,0 +1,15 @@ +{ + "@context": { + "@version": 1.1, + "PresentationSubmission": { + "@id": "https://identity.foundation/presentation-exchange/#presentation-submission", + "@context": { + "@version": 1.1, + "presentation_submission": { + "@id": "https://identity.foundation/presentation-exchange/#presentation-submission", + "@type": "@json" + } + } + } + } +} diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseSerializationTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseSerializationTest.java new file mode 100644 index 00000000..43dc24fe --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseSerializationTest.java @@ -0,0 +1,112 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.dto; + +import com.github.tomakehurst.wiremock.common.StreamSources; +import lombok.SneakyThrows; +import org.eclipse.tractusx.managedidentitywallets.reader.TractusXJsonLdReader; +import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * This test verifies that the serialized output of the Presentation Response DTO is JsonLD and Tractus-X compliant. + *

+ * It does so by comparing the serialized output with a predefined expected output. Like a contract test. + *

+ */ +public class PresentationResponseSerializationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /* Please note: The order of the properties is important, as the Unit Tests does some String comparison. */ + final String Presentation = """ + { + "id": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", + "type": ["VerifiablePresentation", "ExamplePresentation"], + "@context": [ + "https://www.w3.org/ns/credentials/v2" + ], + "verifiableCredential": [{ + "@context": "https://www.w3.org/ns/credentials/v2", + "id": "data:application/vc+sd-jwt;QzVjV...RMjU", + "issuer": "did:example:123", + "issuanceDate": "2020-03-10T04:24:12.164Z", + "type": "VerifiableCredential", + "credentialSubject": { + "id": "did:example:456", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + } + }] + }"""; + + /* Please note: The order of the properties is important, as the Unit Tests does some String comparison. */ + final String ExpectedPresentationResponse = "{\n" + + " \"@context\" : [\n" + + " \"https://w3id.org/tractusx-trust/v0.8\"\n" + + " ],\n" + + " \"@type\" : [\n" + + " \"PresentationResponseMessage\"\n" + + " ],\n" + + " \"presentation\" : [\n" + + Presentation + + " ]\n" + + " }\n" + + " ]\n" + + "}\n"; + + @Test + @SneakyThrows + public void testPresentationResponseSerialization() { + var presentation = getPresentation(); + + var response = new PresentationResponseMessage(presentation); + + var serialized = MAPPER.writeValueAsString(response); + + + var serializedDocument = new StreamSources.StringInputStreamSource(serialized, StandardCharsets.UTF_8).getStream(); + var expectedDocument = new StreamSources.StringInputStreamSource(ExpectedPresentationResponse, StandardCharsets.UTF_8).getStream(); + + var reader = new TractusXJsonLdReader(); + var normalizedSerializedDocument = reader.expand(serializedDocument).toString(); + var normalizedExpectedDocument = reader.expand(expectedDocument).toString(); + + + var isEqual = normalizedSerializedDocument.equals(normalizedExpectedDocument); + + Assertions.assertTrue(isEqual, "Expected both documents to be equal.\n%s\n%s".formatted(normalizedSerializedDocument, normalizedExpectedDocument)); + } + + @SneakyThrows + private VerifiablePresentation getPresentation() { + var map = MAPPER.readValue(Presentation, Map.class); + return new VerifiablePresentation(map); + } +} diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java new file mode 100644 index 00000000..44317ee0 --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java @@ -0,0 +1,161 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.identityminustrust; + +import lombok.SneakyThrows; +import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; +import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; +import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; +import org.eclipse.tractusx.managedidentitywallets.utils.ResourceUtil; +import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; +import org.eclipse.tractusx.ssi.lib.did.web.DidWebFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; +import java.util.Map; + +import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; + + +@DirtiesContext +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) +@ContextConfiguration(initializers = { TestContextInitializer.class }) +public class TokenRequestTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String PRESENTATION_QUERY_REQUEST = "identityminustrust/messages/presentation_query.json"; + + @Autowired + private MIWSettings miwSettings; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private TestRestTemplate testTemplate; + + @Autowired + private IssuersCredentialService issuersCredentialService; + + private String bpn; + + private String clientId; + + private String clientSecret; + + @BeforeEach + @SneakyThrows + public void initWallets() { + // given + bpn = TestUtils.getRandomBpmNumber(); + String partnerBpn = TestUtils.getRandomBpmNumber(); + clientId = bpn; + clientSecret = bpn; + AuthenticationUtils.setupKeycloakClient(clientId, clientSecret, bpn); + AuthenticationUtils.setupKeycloakClient("partner", "partner", partnerBpn); + String did = DidWebFactory.fromHostnameAndPath(miwSettings.host(), bpn).toString(); + String didPartner = DidWebFactory.fromHostnameAndPath(miwSettings.host(), partnerBpn).toString(); + String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + TestUtils.createWallet(bpn, did, testTemplate, miwSettings.authorityWalletBpn(), defaultLocation); + String defaultLocationPartner = miwSettings.host() + COLON_SEPARATOR + partnerBpn; + TestUtils.createWallet(partnerBpn, didPartner, testTemplate, miwSettings.authorityWalletBpn(), defaultLocationPartner); + + var vc = "{\n" + + " \"id\": \"did:web:foo#f255c392-82aa-483a-90a3-3c8697cd246a\",\n" + + " \"@context\": [\n" + + " \"https://www.w3.org/2018/credentials/v1\",\n" + + " \"https://w3id.org/security/suites/jws-2020/v1\"\n" + + " ],\n" + + " \"type\": [\"VerifiableCredential\", \"MembershipCredential\"],\n" + + " \"issuanceDate\": \"2021-06-16T18:56:59Z\",\n" + + " \"expirationDate\": \"2022-06-16T18:56:59Z\",\n" + + " \"issuer\": \"" + miwSettings.authorityWalletDid() + "\",\n" + + " \"credentialSubject\": {\n" + + " \"type\":\"MembershipCredential\",\n" + + " \"holderIdentifier\": \"" + did + "\",\n" + + " \"memberOf\":\"Catena-X\",\n" + + " \"status\":\"Active\",\n" + + " \"startTime656\":\"2021-06-16T18:56:59Z\"\n" + + " }\n" + + "}"; + + issuersCredentialService.issueCredentialUsingBaseWallet( + did, + MAPPER.readValue(vc, Map.class), + false, + miwSettings.authorityWalletBpn() + ); + } + + @Test + @SneakyThrows + public void testPresentationQueryWithToken() { + // when + String body = "audience=%s&client_id=%s&client_secret=%s&grant_type=client_credentials&bearer_access_scope=org.eclipse.tractusx.vc.type:MembershipCredential:read"; + String requestBody = String.format(body, bpn, clientId, clientSecret); + // then + HttpHeaders headers = new HttpHeaders(); + headers.put(HttpHeaders.CONTENT_TYPE, List.of(MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + HttpEntity entity = new HttpEntity<>(requestBody, headers); + ResponseEntity> response = testTemplate.exchange( + "/api/token", + HttpMethod.POST, + entity, + new ParameterizedTypeReference<>() { + } + ); + + var jwt = (String) response.getBody().get("access_token"); + + final String message2 = ResourceUtil.loadResource(PRESENTATION_QUERY_REQUEST); + final Map data2 = MAPPER.readValue(message2, Map.class); + + final HttpHeaders headers2 = new HttpHeaders(); + headers2.set(HttpHeaders.AUTHORIZATION, jwt); + final HttpEntity> entity2 = new HttpEntity<>(data2, headers2); + var result2 = restTemplate + .postForEntity(RestURI.API_PRESENTATIONS_IATP, entity2, String.class); + + System.out.println("RESULT:\n" + result2.toString()); + + Assertions.assertTrue(result2.getStatusCode().is2xxSuccessful()); + } + +} diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/reader/PresentationRequestReaderTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/reader/PresentationRequestReaderTest.java new file mode 100644 index 00000000..75ec3fd5 --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/reader/PresentationRequestReaderTest.java @@ -0,0 +1,49 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.reader; + +import lombok.SneakyThrows; +import org.eclipse.tractusx.managedidentitywallets.utils.ResourceUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.List; + +public class PresentationRequestReaderTest { + + private final TractusXPresentationRequestReader presentationRequestReader = new TractusXPresentationRequestReader(); + + @Test + @SneakyThrows + public void readCredentialsTest() { + + final InputStream is = ResourceUtil.getResourceStream("identityminustrust/messages/presentation_query.json"); + + final List credentialScopes = presentationRequestReader.readVerifiableCredentialScopes(is); + + final String expected = "org.eclipse.tractusx.vc.type:MembershipCredential:read"; + + System.out.printf("Found credentials: %s", credentialScopes.toString()); + Assertions.assertTrue(credentialScopes.contains(expected), "Expected %s".formatted(expected)); + } +} diff --git a/miw/src/test/resources/identityminustrust/messages/presentation_query.json b/miw/src/test/resources/identityminustrust/messages/presentation_query.json new file mode 100644 index 00000000..5b0cd034 --- /dev/null +++ b/miw/src/test/resources/identityminustrust/messages/presentation_query.json @@ -0,0 +1,10 @@ +{ + "scope" : [ + "org.eclipse.tractusx.vc.type:MembershipCredential:read" + ], + "@context" : [ + "https://identity.foundation/presentation-exchange/submission/v1", + "https://w3id.org/tractusx-trust/v0.8" + ], + "@type" : "PresentationQueryMessage" +} From 87e8a5d463b2140ed0071964199e672ec257a4f5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Jul 2024 13:56:49 +0000 Subject: [PATCH 06/60] chore(release): 1.0.0-develop.1 [skip ci] # [1.0.0-develop.1](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.6.0-develop.1...v1.0.0-develop.1) (2024-07-18) * feat(identity-trust)!: update IATP protocol ([e3c5450](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/e3c5450fe2e0084f9deee16dff59e228afa40966)) ### BREAKING CHANGES * `/api/presentations/iatp` endpoint now accepts PresentationQueryMessage and returns PresentationResponseMessage objects. Signed-off-by: Dominik Pinsel --- CHANGELOG.md | 12 ++++++++++++ charts/managed-identity-wallet/Chart.yaml | 4 ++-- charts/managed-identity-wallet/README.md | 2 +- gradle.properties | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4769e864..bfe431ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [1.0.0-develop.1](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.6.0-develop.1...v1.0.0-develop.1) (2024-07-18) + + +* feat(identity-trust)!: update IATP protocol ([e3c5450](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/e3c5450fe2e0084f9deee16dff59e228afa40966)) + + +### BREAKING CHANGES + +* `/api/presentations/iatp` endpoint now accepts PresentationQueryMessage and returns PresentationResponseMessage objects. + +Signed-off-by: Dominik Pinsel + # [0.6.0-develop.1](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.5.0...v0.6.0-develop.1) (2024-07-18) diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 6fafb4c5..698723e3 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -25,8 +25,8 @@ description: | type: application -version: 0.6.0-develop.1 -appVersion: 0.6.0-develop.1 +version: 1.0.0-develop.1 +appVersion: 1.0.0-develop.1 home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 3c750d75..586ed587 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -2,7 +2,7 @@ # managed-identity-wallet -![Version: 0.6.0-develop.1](https://img.shields.io/badge/Version-0.6.0--develop.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.6.0-develop.1](https://img.shields.io/badge/AppVersion-0.6.0--develop.1-informational?style=flat-square) +![Version: 1.0.0-develop.1](https://img.shields.io/badge/Version-1.0.0--develop.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.1](https://img.shields.io/badge/AppVersion-1.0.0--develop.1-informational?style=flat-square) Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. diff --git a/gradle.properties b/gradle.properties index 1ab8b3d0..c5528d38 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ jacocoVersion=0.8.9 springBootVersion=3.1.6 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=0.6.0-develop.1 +applicationVersion=1.0.0-develop.1 openApiVersion=2.1.0 From 010ecab904a0ba4e85c3e4b885fbefb2ed6057e6 Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Mon, 22 Jul 2024 17:40:05 +0530 Subject: [PATCH 07/60] fix: updated spring boot and cloud version --- gradle.properties | 4 +- miw/DEPENDENCIES | 239 +++++++++++++++++++++++----------------------- miw/build.gradle | 16 ---- 3 files changed, 121 insertions(+), 138 deletions(-) diff --git a/gradle.properties b/gradle.properties index c5528d38..74c74f65 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -springCloudVersion=2022.0.3 +springCloudVersion=2023.0.3 testContainerVersion=1.19.3 jacocoVersion=0.8.9 -springBootVersion=3.1.6 +springBootVersion=3.3.2 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx applicationVersion=1.0.0-develop.1 diff --git a/miw/DEPENDENCIES b/miw/DEPENDENCIES index 600d381e..5b080ebb 100644 --- a/miw/DEPENDENCIES +++ b/miw/DEPENDENCIES @@ -1,20 +1,20 @@ -maven/mavencentral/ch.qos.logback/logback-classic/1.4.12, EPL-1.0 AND LGPL-2.1-only, approved, #15230 -maven/mavencentral/ch.qos.logback/logback-core/1.4.12, EPL-1.0 AND LGPL-2.1-only, approved, #15287 +maven/mavencentral/ch.qos.logback/logback-classic/1.5.6, EPL-1.0 AND LGPL-2.1-only, approved, #15279 +maven/mavencentral/ch.qos.logback/logback-core/1.5.6, EPL-1.0 AND LGPL-2.1-only, approved, #15210 maven/mavencentral/com.apicatalog/titanium-json-ld/1.3.3, Apache-2.0, approved, #8912 -maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.15.3, Apache-2.0, approved, #15260 -maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.15.3, , approved, #15194 -maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.15.3, Apache-2.0, approved, #15199 -maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.15.3, Apache-2.0, approved, #9237 -maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.3, Apache-2.0, approved, #15207 -maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.3, Apache-2.0, approved, #15281 -maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.3, Apache-2.0, approved, #15189 -maven/mavencentral/com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.15.3, Apache-2.0, approved, #11061 -maven/mavencentral/com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.15.3, Apache-2.0, approved, #9101 -maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.15.3, Apache-2.0, approved, #9100 -maven/mavencentral/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.3, Apache-2.0, approved, #15219 -maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.15.3, Apache-2.0, approved, #7929 -maven/mavencentral/com.fasterxml.woodstox/woodstox-core/6.5.1, Apache-2.0, approved, #7950 -maven/mavencentral/com.fasterxml/classmate/1.5.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.2, Apache-2.0, approved, #13672 +maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.2, , approved, #13665 +maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.2, Apache-2.0, approved, #13671 +maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.17.2, Apache-2.0, approved, #13666 +maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.17.2, Apache-2.0, approved, #13669 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.17.2, Apache-2.0, approved, #15117 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.2, Apache-2.0, approved, #14160 +maven/mavencentral/com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.17.2, Apache-2.0, approved, #13663 +maven/mavencentral/com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.17.2, Apache-2.0, approved, #13670 +maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.17.2, Apache-2.0, approved, #13664 +maven/mavencentral/com.fasterxml.jackson.module/jackson-module-parameter-names/2.17.2, Apache-2.0, approved, #15122 +maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.17.2, Apache-2.0, approved, #14162 +maven/mavencentral/com.fasterxml.woodstox/woodstox-core/6.7.0, Apache-2.0, approved, #15476 +maven/mavencentral/com.fasterxml/classmate/1.7.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.ben-manes.caffeine/caffeine/3.1.8, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.curious-odd-man/rgxgen/1.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.dasniko/testcontainers-keycloak/2.5.0, Apache-2.0, approved, #9175 @@ -34,17 +34,17 @@ maven/mavencentral/com.google.errorprone/error_prone_annotations/2.21.1, Apache- maven/mavencentral/com.google.protobuf/protobuf-java/3.19.6, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.h2database/h2/2.2.220, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 maven/mavencentral/com.ibm.async/asyncutil/0.1.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.jayway.jsonpath/json-path/2.8.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.1, Apache-2.0, approved, #11701 -maven/mavencentral/com.opencsv/opencsv/5.7.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.jayway.jsonpath/json-path/2.9.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.3, Apache-2.0, approved, #11701 +maven/mavencentral/com.opencsv/opencsv/5.9, Apache-2.0, approved, clearlydefined maven/mavencentral/com.smartsensesolutions/commons-dao/0.0.5, Apache-2.0, approved, #9176 maven/mavencentral/com.sun.activation/jakarta.activation/1.2.1, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/com.sun.istack/istack-commons-runtime/4.1.2, BSD-3-Clause, approved, #15290 maven/mavencentral/com.sun.mail/jakarta.mail/1.6.5, EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, ee4j.mail maven/mavencentral/com.vaadin.external.google/android-json/0.0.20131108.vaadin1, Apache-2.0, approved, CQ21310 -maven/mavencentral/com.zaxxer/HikariCP/5.0.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.zaxxer/HikariCP/5.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/commons-beanutils/commons-beanutils/1.9.4, Apache-2.0, approved, CQ12654 -maven/mavencentral/commons-codec/commons-codec/1.15, Apache-2.0 AND BSD-3-Clause AND LicenseRef-Public-Domain, approved, CQ22641 +maven/mavencentral/commons-codec/commons-codec/1.16.1, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #9157 maven/mavencentral/commons-collections/commons-collections/3.2.2, Apache-2.0, approved, #15185 maven/mavencentral/commons-digester/commons-digester/2.1, Apache-2.0, approved, clearlydefined maven/mavencentral/commons-fileupload/commons-fileupload/1.5, Apache-2.0, approved, #7109 @@ -53,11 +53,12 @@ maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ maven/mavencentral/commons-validator/commons-validator/1.7, Apache-2.0, approved, clearlydefined maven/mavencentral/io.github.openfeign.form/feign-form-spring/3.8.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.github.openfeign.form/feign-form/3.8.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.github.openfeign/feign-core/12.3, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.github.openfeign/feign-slf4j/12.3, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.micrometer/micrometer-commons/1.11.6, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #9243 -maven/mavencentral/io.micrometer/micrometer-core/1.11.6, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #9238 -maven/mavencentral/io.micrometer/micrometer-observation/1.11.6, Apache-2.0, approved, #9242 +maven/mavencentral/io.github.openfeign/feign-core/13.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.github.openfeign/feign-slf4j/13.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.micrometer/micrometer-commons/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14826 +maven/mavencentral/io.micrometer/micrometer-core/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14827 +maven/mavencentral/io.micrometer/micrometer-jakarta9/1.13.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.micrometer/micrometer-observation/1.13.2, Apache-2.0, approved, #14829 maven/mavencentral/io.quarkus/quarkus-junit4-mock/2.13.7.Final, Apache-2.0, approved, clearlydefined maven/mavencentral/io.setl/rdf-urdna/1.2, Apache-2.0, approved, clearlydefined maven/mavencentral/io.smallrye.common/smallrye-common-annotation/1.6.0, Apache-2.0, approved, clearlydefined @@ -68,63 +69,61 @@ maven/mavencentral/io.smallrye.common/smallrye-common-function/1.6.0, Apache-2.0 maven/mavencentral/io.smallrye.config/smallrye-config-common/2.3.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.smallrye.config/smallrye-config-core/2.3.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.smallrye.config/smallrye-config/2.3.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.smallrye/jandex/3.0.5, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.smallrye/jandex/3.1.2, Apache-2.0, approved, clearlydefined maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.9, Apache-2.0, approved, #5947 maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.9, Apache-2.0, approved, #5929 maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.9, Apache-2.0, approved, #5919 -maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.2, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf +maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.3, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/jakarta.annotation/jakarta.annotation-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.ca maven/mavencentral/jakarta.inject/jakarta.inject-api/2.0.1, Apache-2.0, approved, ee4j.cdi maven/mavencentral/jakarta.json/jakarta.json-api/2.1.3, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jsonp maven/mavencentral/jakarta.persistence/jakarta.persistence-api/3.1.0, EPL-2.0 OR BSD-3-Clause, approved, ee4j.jpa maven/mavencentral/jakarta.transaction/jakarta.transaction-api/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jta maven/mavencentral/jakarta.validation/jakarta.validation-api/3.0.2, Apache-2.0, approved, ee4j.validation -maven/mavencentral/jakarta.xml.bind/jakarta.xml.bind-api/4.0.1, BSD-3-Clause, approved, ee4j.jaxb -maven/mavencentral/javax.activation/javax.activation-api/1.2.0, (CDDL-1.1 OR GPL-2.0 WITH Classpath-exception-2.0) AND Apache-2.0, approved, CQ18740 +maven/mavencentral/jakarta.xml.bind/jakarta.xml.bind-api/4.0.2, BSD-3-Clause, approved, ee4j.jaxb maven/mavencentral/javax.xml.bind/jaxb-api/2.3.1, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ16911 maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 -maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.10, Apache-2.0, approved, #7164 -maven/mavencentral/net.bytebuddy/byte-buddy/1.14.10, Apache-2.0 AND BSD-3-Clause, approved, #7163 +maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.18, Apache-2.0, approved, #7164 +maven/mavencentral/net.bytebuddy/byte-buddy/1.14.18, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0-1.0, approved, CQ22537 maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #15196 -maven/mavencentral/net.minidev/accessors-smart/2.4.11, Apache-2.0, approved, #7515 -maven/mavencentral/net.minidev/json-smart/2.4.11, Apache-2.0, approved, #3288 -maven/mavencentral/org.antlr/antlr4-runtime/4.10.1, BSD-3-Clause AND LicenseRef-Public-domain AND MIT AND LicenseRef-Unicode-TOU, approved, #7065 +maven/mavencentral/net.minidev/accessors-smart/2.5.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/net.minidev/json-smart/2.5.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.antlr/antlr4-runtime/4.13.0, BSD-3-Clause, approved, #10767 maven/mavencentral/org.apache.commons/commons-collections4/4.4, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.commons/commons-compress/1.24.0, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, #10368 -maven/mavencentral/org.apache.commons/commons-lang3/3.12.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.apache.commons/commons-text/1.10.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.apache.commons/commons-lang3/3.14.0, Apache-2.0, approved, #11677 +maven/mavencentral/org.apache.commons/commons-text/1.11.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0, approved, #15248 maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.16, Apache-2.0, approved, CQ23528 maven/mavencentral/org.apache.james/apache-mime4j-core/0.8.3, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.james/apache-mime4j-dom/0.8.3, Apache-2.0, approved, #2340 maven/mavencentral/org.apache.james/apache-mime4j-storage/0.8.3, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.apache.logging.log4j/log4j-api/2.20.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.apache.logging.log4j/log4j-to-slf4j/2.20.0, Apache-2.0, approved, #8799 -maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-core/10.1.16, Apache-2.0 AND (EPL-2.0 OR (GPL-2.0 WITH Classpath-exception-2.0)) AND CDDL-1.0 AND (CDDL-1.1 OR (GPL-2.0-only WITH Classpath-exception-2.0)) AND EPL-2.0, approved, #15195 -maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-el/10.1.16, Apache-2.0, approved, #6997 -maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.16, Apache-2.0, approved, #7920 +maven/mavencentral/org.apache.logging.log4j/log4j-api/2.23.1, Apache-2.0, approved, #13368 +maven/mavencentral/org.apache.logging.log4j/log4j-to-slf4j/2.23.1, Apache-2.0, approved, #15121 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-core/10.1.26, Apache-2.0 AND (EPL-2.0 OR (GPL-2.0 WITH Classpath-exception-2.0)) AND CDDL-1.0 AND (CDDL-1.1 OR (GPL-2.0-only WITH Classpath-exception-2.0)) AND EPL-2.0, approved, #15195 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-el/10.1.26, Apache-2.0, approved, #6997 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.26, Apache-2.0, approved, #7920 maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.aspectj/aspectjweaver/1.9.20.1, Apache-2.0 AND BSD-3-Clause AND EPL-1.0 AND BSD-3-Clause AND Apache-1.1, approved, #7695 -maven/mavencentral/org.assertj/assertj-core/3.24.2, Apache-2.0, approved, #6161 -maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.69, MIT, approved, clearlydefined -maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.69, MIT, approved, clearlydefined -maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.77, MIT AND CC0-1.0, approved, #11595 -maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.69, MIT, approved, clearlydefined +maven/mavencentral/org.aspectj/aspectjweaver/1.9.22.1, Apache-2.0 AND BSD-3-Clause AND EPL-1.0 AND BSD-3-Clause AND Apache-1.1, approved, #15252 +maven/mavencentral/org.assertj/assertj-core/3.25.3, Apache-2.0, approved, #12585 +maven/mavencentral/org.awaitility/awaitility/4.2.1, Apache-2.0, approved, #14178 +maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.78, MIT AND CC0-1.0, approved, #14433 maven/mavencentral/org.checkerframework/checker-qual/3.37.0, MIT, approved, clearlydefined -maven/mavencentral/org.codehaus.woodstox/stax2-api/4.2.1, BSD-2-Clause, approved, #2670 -maven/mavencentral/org.eclipse.angus/angus-activation/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus +maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined +maven/mavencentral/org.codehaus.woodstox/stax2-api/4.2.2, BSD-2-Clause, approved, #2670 +maven/mavencentral/org.eclipse.angus/angus-activation/2.0.2, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus maven/mavencentral/org.eclipse.microprofile.config/microprofile-config-api/2.0, Apache-2.0, approved, technology.microprofile maven/mavencentral/org.eclipse.parsson/parsson/1.1.5, EPL-2.0, approved, ee4j.parsson maven/mavencentral/org.eclipse.tractusx.ssi/cx-ssi-lib/0.0.19, Apache-2.0, approved, automotive.tractusx -maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl -maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl -maven/mavencentral/org.glassfish.jaxb/txw2/4.0.4, BSD-3-Clause, approved, ee4j.jaxb-impl +maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.5, BSD-3-Clause, approved, ee4j.jaxb-impl +maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.5, BSD-3-Clause, approved, ee4j.jaxb-impl +maven/mavencentral/org.glassfish.jaxb/txw2/4.0.5, BSD-3-Clause, approved, ee4j.jaxb-impl maven/mavencentral/org.hamcrest/hamcrest-core/2.2, BSD-3-Clause, approved, clearlydefined maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-3-Clause, approved, clearlydefined -maven/mavencentral/org.hdrhistogram/HdrHistogram/2.1.12, CC0-1.0, approved, #15259 +maven/mavencentral/org.hdrhistogram/HdrHistogram/2.2.2, BSD-2-Clause AND CC0-1.0 AND CC0-1.0, approved, #14828 maven/mavencentral/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final, LGPL-2.1-only, approved, #6962 -maven/mavencentral/org.hibernate.orm/hibernate-core/6.2.13.Final, LGPL-2.1-only AND Apache-2.0 AND MIT AND CC-PDDC AND (EPL-2.0 OR BSD-3-Clause), approved, #9121 +maven/mavencentral/org.hibernate.orm/hibernate-core/6.5.2.Final, LGPL-2.1-only AND (EPL-2.0 OR BSD-3-Clause) AND LGPL-2.1-or-later AND MIT, approved, #15118 maven/mavencentral/org.hibernate.validator/hibernate-validator/8.0.1.Final, Apache-2.0, approved, clearlydefined maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.9, EPL-2.0, approved, CQ23285 maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.9, EPL-2.0, approved, #1068 @@ -146,85 +145,85 @@ maven/mavencentral/org.jboss.spec.javax.ws.rs/jboss-jaxrs-api_2.1_spec/2.0.1.Fin maven/mavencentral/org.jboss.spec.javax.xml.bind/jboss-jaxb-api_2.3_spec/2.0.0.Final, BSD-3-Clause, approved, #2122 maven/mavencentral/org.jetbrains/annotations/17.0.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.json/json/20230227, LicenseRef-Public-domain, approved, #9174 -maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.9.3, EPL-2.0, approved, #3133 -maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.9.3, EPL-2.0, approved, #3125 -maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.9.3, EPL-2.0, approved, #3134 -maven/mavencentral/org.junit.jupiter/junit-jupiter/5.9.3, EPL-2.0, approved, #6972 -maven/mavencentral/org.junit.platform/junit-platform-commons/1.9.3, EPL-2.0, approved, #3130 -maven/mavencentral/org.junit.platform/junit-platform-engine/1.9.3, EPL-2.0, approved, #3128 -maven/mavencentral/org.junit/junit-bom/5.9.3, EPL-2.0, approved, #4711 +maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.3, EPL-2.0, approved, #9714 +maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.3, EPL-2.0, approved, #9711 +maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.3, EPL-2.0, approved, #15250 +maven/mavencentral/org.junit.jupiter/junit-jupiter/5.10.3, EPL-2.0, approved, #15197 +maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.3, EPL-2.0, approved, #9715 +maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.3, EPL-2.0, approved, #9709 +maven/mavencentral/org.junit/junit-bom/5.10.3, EPL-2.0, approved, #9844 maven/mavencentral/org.keycloak/keycloak-admin-client/21.0.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.keycloak/keycloak-common/21.0.0, Apache-2.0 AND LicenseRef-scancode-public-domain-disclaimer, approved, #7287 maven/mavencentral/org.keycloak/keycloak-core/21.0.0, Apache-2.0, approved, #7293 maven/mavencentral/org.latencyutils/LatencyUtils/2.0.3, CC0-1.0, approved, #15280 -maven/mavencentral/org.liquibase/liquibase-core/4.20.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.mockito/mockito-core/4.8.1, MIT, approved, clearlydefined +maven/mavencentral/org.liquibase/liquibase-core/4.27.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.mockito/mockito-core/5.11.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #13505 maven/mavencentral/org.mockito/mockito-inline/5.2.0, MIT, approved, clearlydefined -maven/mavencentral/org.mockito/mockito-junit-jupiter/4.8.1, MIT, approved, clearlydefined -maven/mavencentral/org.objenesis/objenesis/3.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.opentest4j/opentest4j/1.2.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.mockito/mockito-junit-jupiter/5.11.0, MIT, approved, #13504 +maven/mavencentral/org.objenesis/objenesis/3.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.opentest4j/opentest4j/1.3.0, Apache-2.0, approved, #9713 maven/mavencentral/org.ow2.asm/asm-commons/9.5, BSD-3-Clause, approved, #7553 maven/mavencentral/org.ow2.asm/asm-tree/9.5, BSD-3-Clause, approved, #7555 -maven/mavencentral/org.ow2.asm/asm/9.3, BSD-3-Clause, approved, clearlydefined maven/mavencentral/org.ow2.asm/asm/9.5, BSD-3-Clause, approved, #7554 -maven/mavencentral/org.postgresql/postgresql/42.6.0, BSD-2-Clause AND Apache-2.0, approved, #9159 +maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 +maven/mavencentral/org.postgresql/postgresql/42.7.3, BSD-2-Clause AND Apache-2.0, approved, #11681 maven/mavencentral/org.projectlombok/lombok/1.18.28, MIT, approved, #15192 -maven/mavencentral/org.projectlombok/lombok/1.18.30, MIT, approved, #15192 +maven/mavencentral/org.projectlombok/lombok/1.18.34, MIT, approved, #15192 maven/mavencentral/org.reactivestreams/reactive-streams/1.0.4, CC0-1.0, approved, CQ16332 maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined -maven/mavencentral/org.skyscreamer/jsonassert/1.5.1, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.slf4j/jul-to-slf4j/2.0.9, MIT, approved, #7698 -maven/mavencentral/org.slf4j/slf4j-api/2.0.9, MIT, approved, #5915 +maven/mavencentral/org.skyscreamer/jsonassert/1.5.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.slf4j/jul-to-slf4j/2.0.13, MIT, approved, #7698 +maven/mavencentral/org.slf4j/slf4j-api/2.0.13, MIT, approved, #5915 maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/2.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/2.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.1.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/3.1.6, Apache-2.0, approved, #9348 -maven/mavencentral/org.springframework.boot/spring-boot-actuator/3.1.6, Apache-2.0, approved, #9342 -maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/3.1.6, Apache-2.0, approved, #9341 -maven/mavencentral/org.springframework.boot/spring-boot-devtools/3.1.5, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.boot/spring-boot-starter-actuator/3.1.6, Apache-2.0, approved, #9344 -maven/mavencentral/org.springframework.boot/spring-boot-starter-aop/3.1.6, Apache-2.0, approved, #9338 -maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/3.1.6, Apache-2.0, approved, #9733 -maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/3.1.6, Apache-2.0, approved, #9737 -maven/mavencentral/org.springframework.boot/spring-boot-starter-json/3.1.6, Apache-2.0, approved, #9336 -maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/3.1.6, Apache-2.0, approved, #9343 -maven/mavencentral/org.springframework.boot/spring-boot-starter-security/3.1.6, Apache-2.0, approved, #9337 -maven/mavencentral/org.springframework.boot/spring-boot-starter-test/3.1.6, Apache-2.0, approved, #9353 -maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/3.1.6, Apache-2.0, approved, #9351 -maven/mavencentral/org.springframework.boot/spring-boot-starter-validation/3.1.6, Apache-2.0, approved, #9335 -maven/mavencentral/org.springframework.boot/spring-boot-starter-web/3.1.6, Apache-2.0, approved, #9347 -maven/mavencentral/org.springframework.boot/spring-boot-starter/3.1.6, Apache-2.0, approved, #9349 -maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/3.1.6, Apache-2.0, approved, #9339 -maven/mavencentral/org.springframework.boot/spring-boot-test/3.1.6, Apache-2.0, approved, #9346 -maven/mavencentral/org.springframework.boot/spring-boot/3.1.6, Apache-2.0, approved, #9352 -maven/mavencentral/org.springframework.cloud/spring-cloud-commons/4.0.3, Apache-2.0, approved, #7292 -maven/mavencentral/org.springframework.cloud/spring-cloud-context/4.0.3, Apache-2.0, approved, #7306 -maven/mavencentral/org.springframework.cloud/spring-cloud-openfeign-core/4.0.3, Apache-2.0, approved, #7305 -maven/mavencentral/org.springframework.cloud/spring-cloud-starter-openfeign/4.0.3, Apache-2.0, approved, #7302 -maven/mavencentral/org.springframework.cloud/spring-cloud-starter/4.0.3, Apache-2.0, approved, #7299 -maven/mavencentral/org.springframework.data/spring-data-commons/3.1.6, Apache-2.0, approved, #8805 -maven/mavencentral/org.springframework.data/spring-data-jpa/3.1.6, Apache-2.0, approved, #9120 -maven/mavencentral/org.springframework.security/spring-security-config/6.1.5, Apache-2.0, approved, #9736 -maven/mavencentral/org.springframework.security/spring-security-core/6.1.5, Apache-2.0, approved, #9801 -maven/mavencentral/org.springframework.security/spring-security-crypto/6.1.5, Apache-2.0 AND ISC, approved, #9735 -maven/mavencentral/org.springframework.security/spring-security-oauth2-core/6.1.5, Apache-2.0, approved, #9741 -maven/mavencentral/org.springframework.security/spring-security-oauth2-jose/6.1.5, Apache-2.0, approved, #9345 -maven/mavencentral/org.springframework.security/spring-security-oauth2-resource-server/6.1.5, Apache-2.0, approved, #8798 -maven/mavencentral/org.springframework.security/spring-security-rsa/1.0.11.RELEASE, Apache-2.0, approved, CQ20647 -maven/mavencentral/org.springframework.security/spring-security-web/6.1.5, Apache-2.0, approved, #9800 -maven/mavencentral/org.springframework/spring-aop/6.0.14, Apache-2.0, approved, #5940 -maven/mavencentral/org.springframework/spring-aspects/6.0.14, Apache-2.0, approved, #5930 -maven/mavencentral/org.springframework/spring-beans/6.0.14, Apache-2.0, approved, #5937 -maven/mavencentral/org.springframework/spring-context/6.0.14, Apache-2.0, approved, #5936 -maven/mavencentral/org.springframework/spring-core/6.0.14, Apache-2.0 AND BSD-3-Clause, approved, #5948 -maven/mavencentral/org.springframework/spring-expression/6.0.14, Apache-2.0, approved, #3284 -maven/mavencentral/org.springframework/spring-jcl/6.0.14, Apache-2.0, approved, #3283 -maven/mavencentral/org.springframework/spring-jdbc/6.0.14, Apache-2.0, approved, #5924 -maven/mavencentral/org.springframework/spring-orm/6.0.14, Apache-2.0, approved, #5925 -maven/mavencentral/org.springframework/spring-test/6.0.14, Apache-2.0, approved, #7003 -maven/mavencentral/org.springframework/spring-tx/6.0.14, Apache-2.0, approved, #5926 -maven/mavencentral/org.springframework/spring-web/6.0.14, Apache-2.0, approved, #5942 -maven/mavencentral/org.springframework/spring-webmvc/6.0.14, Apache-2.0, approved, #5944 +maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-actuator/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-devtools/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-actuator/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-aop/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-json/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-security/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-test/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-validation/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-web/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-test/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.cloud/spring-cloud-commons/4.1.4, Apache-2.0, approved, #13495 +maven/mavencentral/org.springframework.cloud/spring-cloud-context/4.1.4, Apache-2.0, approved, #13494 +maven/mavencentral/org.springframework.cloud/spring-cloud-openfeign-core/4.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.cloud/spring-cloud-starter-openfeign/4.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.cloud/spring-cloud-starter/4.1.4, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.data/spring-data-commons/3.3.2, Apache-2.0, approved, #15116 +maven/mavencentral/org.springframework.data/spring-data-jpa/3.3.2, Apache-2.0, approved, #15120 +maven/mavencentral/org.springframework.security/spring-security-config/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-core/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-crypto/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-core/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-jose/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-resource-server/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-rsa/1.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-web/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework/spring-aop/6.1.11, Apache-2.0, approved, #15221 +maven/mavencentral/org.springframework/spring-aspects/6.1.11, Apache-2.0, approved, #15193 +maven/mavencentral/org.springframework/spring-beans/6.1.11, Apache-2.0, approved, #15213 +maven/mavencentral/org.springframework/spring-context/6.1.11, Apache-2.0, approved, #15261 +maven/mavencentral/org.springframework/spring-core/6.1.11, Apache-2.0 AND BSD-3-Clause, approved, #15206 +maven/mavencentral/org.springframework/spring-expression/6.1.11, Apache-2.0, approved, #15264 +maven/mavencentral/org.springframework/spring-jcl/6.1.11, Apache-2.0, approved, #15266 +maven/mavencentral/org.springframework/spring-jdbc/6.1.11, Apache-2.0, approved, #15191 +maven/mavencentral/org.springframework/spring-orm/6.1.11, Apache-2.0, approved, #15278 +maven/mavencentral/org.springframework/spring-test/6.1.11, Apache-2.0, approved, #15265 +maven/mavencentral/org.springframework/spring-tx/6.1.11, Apache-2.0, approved, #15229 +maven/mavencentral/org.springframework/spring-web/6.1.11, Apache-2.0, approved, #15188 +maven/mavencentral/org.springframework/spring-webmvc/6.1.11, Apache-2.0, approved, #15182 maven/mavencentral/org.testcontainers/database-commons/1.19.3, Apache-2.0, approved, #10345 maven/mavencentral/org.testcontainers/jdbc/1.19.3, Apache-2.0, approved, #10348 maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 diff --git a/miw/build.gradle b/miw/build.gradle index 9312d372..3ff75754 100644 --- a/miw/build.gradle +++ b/miw/build.gradle @@ -43,22 +43,6 @@ configurations { compileOnly.extendsFrom(annotaionProcessor) } -// this needs to be done, as we're using the org.springframework.boot plugin -// with native Gradle bom resolution -// docs: https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#managing-dependencies.gradle-bom-support.customizing -// docs: https://docs.gradle.org/7.6/dsl/org.gradle.api.artifacts.ResolutionStrategy.html -configurations.configureEach { - resolutionStrategy.eachDependency { DependencyResolveDetails details -> - if (details.requested.group == 'ch.qos.logback') { - details.useVersion '1.4.12' - } - // avoid a license issue - if (details.requested.name == 'spring-boot-devtools') { - details.useVersion '3.1.5' - } - } -} - repositories { // delegate is RepositoryHandler // docs: https://docs.gradle.org/7.6/dsl/org.gradle.api.artifacts.dsl.RepositoryHandler.html From 89c07c5e6c0ff3c1826cc73fc52f80df59bc041e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 22 Jul 2024 13:29:33 +0000 Subject: [PATCH 08/60] chore(release): 1.0.0-develop.2 [skip ci] # [1.0.0-develop.2](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.1...v1.0.0-develop.2) (2024-07-22) ### Bug Fixes * updated spring boot and cloud version ([010ecab](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/010ecab904a0ba4e85c3e4b885fbefb2ed6057e6)) --- CHANGELOG.md | 7 +++++++ charts/managed-identity-wallet/Chart.yaml | 4 ++-- charts/managed-identity-wallet/README.md | 2 +- gradle.properties | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfe431ab..96d95b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.0.0-develop.2](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.1...v1.0.0-develop.2) (2024-07-22) + + +### Bug Fixes + +* updated spring boot and cloud version ([010ecab](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/010ecab904a0ba4e85c3e4b885fbefb2ed6057e6)) + # [1.0.0-develop.1](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v0.6.0-develop.1...v1.0.0-develop.1) (2024-07-18) diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 698723e3..cb8af8d5 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -25,8 +25,8 @@ description: | type: application -version: 1.0.0-develop.1 -appVersion: 1.0.0-develop.1 +version: 1.0.0-develop.2 +appVersion: 1.0.0-develop.2 home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 586ed587..8e004f9e 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -2,7 +2,7 @@ # managed-identity-wallet -![Version: 1.0.0-develop.1](https://img.shields.io/badge/Version-1.0.0--develop.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.1](https://img.shields.io/badge/AppVersion-1.0.0--develop.1-informational?style=flat-square) +![Version: 1.0.0-develop.2](https://img.shields.io/badge/Version-1.0.0--develop.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.2](https://img.shields.io/badge/AppVersion-1.0.0--develop.2-informational?style=flat-square) Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. diff --git a/gradle.properties b/gradle.properties index 74c74f65..fe8ac71d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ jacocoVersion=0.8.9 springBootVersion=3.3.2 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=1.0.0-develop.1 +applicationVersion=1.0.0-develop.2 openApiVersion=2.1.0 From 15425beccd8bbb3560328d7d845766f422e6e4d8 Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Tue, 23 Jul 2024 12:03:34 +0530 Subject: [PATCH 09/60] feat: remove BaseController, change Principal to Authenticationand unit test cases added --- .../CustomAuthenticationEntryPoint.java | 103 ++++++++++++++++ .../config/security/SecurityConfig.java | 31 ++++- .../constant/StringPool.java | 1 + .../controller/DidDocumentController.java | 2 +- .../HoldersCredentialController.java | 23 ++-- .../IssuersCredentialController.java | 21 ++-- .../controller/PresentationController.java | 19 +-- .../controller/WalletController.java | 23 ++-- .../BpnValidator.java} | 35 +++--- .../utils/TokenParsingUtils.java | 15 +++ .../CustomAuthenticationEntryPointTest.java | 114 ++++++++++++++++++ .../utils/BpnValidatorTest.java | 89 ++++++++++++++ 12 files changed, 412 insertions(+), 64 deletions(-) create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java rename miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/{controller/BaseController.java => utils/BpnValidator.java} (58%) create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..6764d63f --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,103 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.config.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Setter; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.util.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The type Custom authentication entry point. + */ +@Setter +public final class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private String realmName; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + HttpStatus status = HttpStatus.UNAUTHORIZED; + Map parameters = new LinkedHashMap<>(); + + if (this.realmName != null) { + parameters.put("realm", this.realmName); + } + + if (authException instanceof OAuth2AuthenticationException) { + OAuth2Error error = ((OAuth2AuthenticationException) authException).getError(); + parameters.put("error", error.getErrorCode()); + if (StringUtils.hasText(error.getDescription())) { + parameters.put("error_description", error.getDescription()); + } + + if (StringUtils.hasText(error.getUri())) { + parameters.put("error_uri", error.getUri()); + } + + if (error instanceof BearerTokenError bearerTokenError) { + if (StringUtils.hasText(bearerTokenError.getScope())) { + parameters.put("scope", bearerTokenError.getScope()); + } + + status = ((BearerTokenError) error).getHttpStatus(); + } + } + + if (authException.getMessage().contains(StringPool.BPN_NOT_FOUND)) { + status = HttpStatus.FORBIDDEN; + } + + String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters); + response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate); + response.setStatus(status.value()); + } + + private static String computeWWWAuthenticateHeaderValue(Map parameters) { + StringBuilder wwwAuthenticate = new StringBuilder(); + wwwAuthenticate.append("Bearer"); + if (!parameters.isEmpty()) { + wwwAuthenticate.append(" "); + int i = 0; + for (Map.Entry entry : parameters.entrySet()) { + wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\""); + if (i != parameters.size() - 1) { + wwwAuthenticate.append(", "); + } + i++; + } + } + return wwwAuthenticate.toString(); + } + +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index 148f1875..937060e1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -21,11 +21,13 @@ package org.eclipse.tractusx.managedidentitywallets.config.security; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.constant.ApplicationRole; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; +import org.eclipse.tractusx.managedidentitywallets.utils.BpnValidator; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -39,6 +41,12 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoders; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -53,13 +61,16 @@ @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) @Configuration -@AllArgsConstructor +@RequiredArgsConstructor public class SecurityConfig { private final STSTokenValidationService validationService; private final SecurityConfigProperties securityConfigProperties; + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + /** * Filter chain security filter chain. * @@ -115,7 +126,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //error .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() ).oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> - jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(securityConfigProperties.clientId())))) + jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(securityConfigProperties.clientId()))) + .authenticationEntryPoint(new CustomAuthenticationEntryPoint())) .addFilterAfter(new PresentationIatpFilter(validationService), BasicAuthenticationFilter.class); return http.build(); @@ -141,4 +153,17 @@ public WebSecurityCustomizer securityCustomizer() { (ApplicationEventPublisher applicationEventPublisher) { return new DefaultAuthenticationEventPublisher(applicationEventPublisher); } + + @Bean + JwtDecoder jwtDecoder() { + NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri); + OAuth2TokenValidator bpnValidator = bpnValidator(); + OAuth2TokenValidator withBpn = new DelegatingOAuth2TokenValidator<>(bpnValidator); + jwtDecoder.setJwtValidator(withBpn); + return jwtDecoder; + } + + OAuth2TokenValidator bpnValidator() { + return new BpnValidator(); + } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java index c14c7530..f63ae081 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java @@ -101,4 +101,5 @@ private StringPool() { public static final String SECURITY_TOKEN_SERVICE = "SecurityTokenService"; public static final String CREDENTIAL_SERVICE = "CredentialService"; public static final String HTTPS_SCHEME = "https://"; + public static final String BPN_NOT_FOUND = "BPN not found"; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java index 6869d453..298fdc10 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java @@ -45,7 +45,7 @@ @RequiredArgsConstructor @Tag(name = "DIDDocument") @Slf4j -public class DidDocumentController extends BaseController { +public class DidDocumentController { private final DidDocumentService service; /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java index 9d55124d..e037506f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java @@ -37,17 +37,18 @@ import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.HoldersCredentialService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -58,7 +59,7 @@ @RequiredArgsConstructor @Slf4j @Tag(name = "Verifiable Credential - Holder") -public class HoldersCredentialController extends BaseController { +public class HoldersCredentialController { private final HoldersCredentialService holdersCredentialService; @@ -71,7 +72,7 @@ public class HoldersCredentialController extends BaseController { * @param type the type * @param sortColumn the sort column * @param sortTpe the sort tpe - * @param principal the principal + * @param authentication the authentication * @return the credentials */ @GetCredentialsApiDocs @@ -94,8 +95,8 @@ public ResponseEntity> getCredentials(@Parameter(n @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt, - Principal principal) { - log.debug("Received request to get credentials. BPN: {}", getBPNFromToken(principal)); + Authentication authentication) { + log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); final GetCredentialsCommand command; command = GetCredentialsCommand.builder() .credentialId(credentialId) @@ -106,7 +107,7 @@ public ResponseEntity> getCredentials(@Parameter(n .pageNumber(pageNumber) .size(size) .asJwt(asJwt) - .callerBPN(getBPNFromToken(principal)) + .callerBPN(TokenParsingUtils.getBPNFromToken(authentication)) .build(); return ResponseEntity.status(HttpStatus.OK).body(holdersCredentialService.getCredentials(command)); } @@ -115,17 +116,17 @@ public ResponseEntity> getCredentials(@Parameter(n /** * Issue credential response entity. * - * @param data the data - * @param principal the principal + * @param data the data + * @param authentication the authentication * @return the response entity */ @IssueCredentialApiDoc @PostMapping(path = RestURI.CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity issueCredential(@RequestBody Map data, Principal principal, + public ResponseEntity issueCredential(@RequestBody Map data, Authentication authentication, @AsJwtParam @RequestParam(name = "asJwt", defaultValue = "false") boolean asJwt ) { - log.debug("Received request to issue credential. BPN: {}", getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, getBPNFromToken(principal), asJwt)); + log.debug("Received request to issue credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, TokenParsingUtils.getBPNFromToken(authentication), asJwt)); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java index 5d3ca437..9ade312f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java @@ -37,17 +37,18 @@ import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -57,7 +58,7 @@ @RestController @RequiredArgsConstructor @Slf4j -public class IssuersCredentialController extends BaseController { +public class IssuersCredentialController { /** * The constant API_TAG_VERIFIABLE_CREDENTIAL_ISSUER. @@ -81,7 +82,7 @@ public class IssuersCredentialController extends BaseController { * @param size the size * @param sortColumn the sort column * @param sortTpe the sort tpe - * @param principal the principal + * @param authentication the authentication * @return the credentials */ @GetCredentialsApiDocs @@ -101,8 +102,8 @@ public ResponseEntity> getCredentials(@Parameter(n ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, @Parameter(name = "sortTpe", description = "Sort order", examples = { @ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order") }) @RequestParam(required = false, defaultValue = "desc") String sortTpe, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt, - Principal principal) { - log.debug("Received request to get credentials. BPN: {}", getBPNFromToken(principal)); + Authentication authentication) { + log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); final GetCredentialsCommand command; command = GetCredentialsCommand.builder() .credentialId(credentialId) @@ -113,7 +114,7 @@ public ResponseEntity> getCredentials(@Parameter(n .pageNumber(pageNumber) .size(size) .asJwt(asJwt) - .callerBPN(getBPNFromToken(principal)) + .callerBPN(TokenParsingUtils.getBPNFromToken(authentication)) .build(); return ResponseEntity.status(HttpStatus.OK).body(issuersCredentialService.getCredentials(command)); } @@ -139,14 +140,14 @@ public ResponseEntity> credentialsValidation(@RequestBody Cr * * @param holderDid the holder did * @param data the data - * @param principal the principal + * @param authentication the authentication * @return the response entity */ @PostMapping(path = RestURI.ISSUERS_CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @IssueVerifiableCredentialUsingBaseWalletApiDocs - public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = {@ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Principal principal, + public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = {@ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Authentication authentication, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt) { - log.debug("Received request to issue verifiable credential. BPN: {}", getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, getBPNFromToken(principal))); + log.debug("Received request to issue verifiable credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, TokenParsingUtils.getBPNFromToken(authentication))); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index 3f5b5a0c..31567822 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -33,10 +33,12 @@ import org.eclipse.tractusx.managedidentitywallets.dto.PresentationResponseMessage; import org.eclipse.tractusx.managedidentitywallets.reader.TractusXPresentationRequestReader; import org.eclipse.tractusx.managedidentitywallets.service.PresentationService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -44,7 +46,6 @@ import org.springframework.web.bind.annotation.RestController; import java.io.InputStream; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -56,7 +57,7 @@ @RestController @RequiredArgsConstructor @Slf4j -public class PresentationController extends BaseController { +public class PresentationController { private final PresentationService presentationService; @@ -65,20 +66,20 @@ public class PresentationController extends BaseController { /** * Create presentation response entity. * - * @param data the data - * @param audience the audience - * @param asJwt the as jwt - * @param principal the principal + * @param data the data + * @param audience the audience + * @param asJwt the as jwt + * @param authentication the authentication * @return the response entity */ @PostVerifiablePresentationApiDocs @PostMapping(path = RestURI.API_PRESENTATIONS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> createPresentation(@RequestBody Map data, @RequestParam(name = "audience", required = false) String audience, - @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, Principal principal + @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, Authentication authentication ) { - log.debug("Received request to create presentation. BPN: {}", getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(presentationService.createPresentation(data, asJwt, audience, getBPNFromToken(principal))); + log.debug("Received request to create presentation. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(presentationService.createPresentation(data, asJwt, audience, TokenParsingUtils.getBPNFromToken(authentication))); } /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java index fb41bdf0..d38e353e 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java @@ -39,13 +39,14 @@ import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; import org.eclipse.tractusx.managedidentitywallets.service.WalletService; +import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; -import java.security.Principal; import java.util.Map; /** @@ -55,7 +56,7 @@ @RequiredArgsConstructor @Slf4j @Tag(name = "Wallets") -public class WalletController extends BaseController { +public class WalletController { private final WalletService service; @@ -67,9 +68,9 @@ public class WalletController extends BaseController { */ @CreateWalletApiDoc @PostMapping(path = RestURI.WALLETS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWallet(@Valid @RequestBody CreateWalletRequest request, Principal principal) { - log.debug("Received request to create wallet with BPN {}. authorized by BPN: {}", request.getBusinessPartnerNumber(), getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(service.createWallet(request, getBPNFromToken(principal))); + public ResponseEntity createWallet(@Valid @RequestBody CreateWalletRequest request, Authentication authentication) { + log.debug("Received request to create wallet with BPN {}. authorized by BPN: {}", request.getBusinessPartnerNumber(), TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(service.createWallet(request, TokenParsingUtils.getBPNFromToken(authentication))); } /** @@ -82,9 +83,9 @@ public ResponseEntity createWallet(@Valid @RequestBody CreateWalletReque @StoreVerifiableCredentialApiDoc @PostMapping(path = RestURI.API_WALLETS_IDENTIFIER_CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> storeCredential(@RequestBody Map data, - @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, Principal principal) { - log.debug("Received request to store credential in wallet with identifier {}. authorized by BPN: {}", identifier, getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.CREATED).body(service.storeCredential(data, identifier, getBPNFromToken(principal))); + @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, Authentication authentication) { + log.debug("Received request to store credential in wallet with identifier {}. authorized by BPN: {}", identifier, TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.CREATED).body(service.storeCredential(data, identifier, TokenParsingUtils.getBPNFromToken(authentication))); } /** @@ -98,9 +99,9 @@ public ResponseEntity> storeCredential(@RequestBody Map getWalletByIdentifier( @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, @RequestParam(name = "withCredentials", defaultValue = "false") boolean withCredentials, - Principal principal) { - log.debug("Received request to retrieve wallet with identifier {}. authorized by BPN: {}", identifier, getBPNFromToken(principal)); - return ResponseEntity.status(HttpStatus.OK).body(service.getWalletByIdentifier(identifier, withCredentials, getBPNFromToken(principal))); + Authentication authentication) { + log.debug("Received request to retrieve wallet with identifier {}. authorized by BPN: {}", identifier, TokenParsingUtils.getBPNFromToken(authentication)); + return ResponseEntity.status(HttpStatus.OK).body(service.getWalletByIdentifier(identifier, withCredentials, TokenParsingUtils.getBPNFromToken(authentication))); } /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/BaseController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java similarity index 58% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/BaseController.java rename to miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java index dd3f958e..b701cf3b 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/BaseController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -19,38 +19,35 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.controller; +package org.eclipse.tractusx.managedidentitywallets.utils; import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; -import java.security.Principal; import java.util.Map; import java.util.TreeMap; /** - * The type Base controller. + * The type Bpn validator. */ -public class BaseController { +public class BpnValidator implements OAuth2TokenValidator { - /** - * Gets bpn from token. - * - * @param principal the principal - * @return the bpn from token - */ - public String getBPNFromToken(Principal principal) { - Object principal1 = ((JwtAuthenticationToken) principal).getPrincipal(); - Jwt jwt = (Jwt) principal1; + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, StringPool.BPN_NOT_FOUND, null); + @Override + public OAuth2TokenValidatorResult validate(Jwt jwt) { //this will misbehave if we have more then one claims with different case // ie. BPN=123456 and bpn=789456 Map claims = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); claims.putAll(jwt.getClaims()); - Validate.isFalse(claims.containsKey(StringPool.BPN)).launch(new ForbiddenException("Invalid token, BPN not found")); - return claims.get(StringPool.BPN).toString(); + if (claims.containsKey(StringPool.BPN)) { + return OAuth2TokenValidatorResult.success(); + } else { + return OAuth2TokenValidatorResult.failure(error); + } } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java index e5101f11..88b6bddb 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java @@ -25,10 +25,16 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import lombok.experimental.UtilityClass; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import java.text.ParseException; +import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; @@ -109,4 +115,13 @@ public static String getNonceAccessToken(JWT accessToken) { throw new BadDataException(PARSING_TOKEN_ERROR, e); } } + + public static String getBPNFromToken(Authentication authentication) { + Jwt jwt = ((JwtAuthenticationToken) authentication).getToken(); + // this will misbehave if we have more then one claims with different case + // ie. BPN=123456 and bpn=789456 + Map claims = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + claims.putAll(jwt.getClaims()); + return claims.get(StringPool.BPN).toString(); + } } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java new file mode 100644 index 00000000..0f1a18a4 --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java @@ -0,0 +1,114 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.tractusx.managedidentitywallets.config.security.CustomAuthenticationEntryPoint; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.server.resource.BearerTokenError; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +class CustomAuthenticationEntryPointTest { + + private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private HttpServletRequest request; + private HttpServletResponse response; + + @BeforeEach + void setUp() { + customAuthenticationEntryPoint = new CustomAuthenticationEntryPoint(); + request = Mockito.mock(HttpServletRequest.class); + response = Mockito.mock(HttpServletResponse.class); + } + + @Test + @DisplayName("Commence should set unauthorized status and headers when OAuth2 authentication exception") + void commenceShouldSetUnauthorizedStatusAndHeadersWhenOAuth2AuthenticationException() { + OAuth2Error error = new OAuth2Error("invalid_token", "The token is invalid", "https://example.com"); + OAuth2AuthenticationException authException = new OAuth2AuthenticationException(error); + + customAuthenticationEntryPoint.commence(request, response, authException); + + ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(String.class); + verify(response).addHeader(eq(HttpHeaders.WWW_AUTHENTICATE), headerCaptor.capture()); + verify(response).setStatus(HttpStatus.UNAUTHORIZED.value()); + + String wwwAuthenticate = headerCaptor.getValue(); + assertEquals("Bearer error=\"invalid_token\", error_description=\"The token is invalid\", error_uri=\"https://example.com\"", wwwAuthenticate); + } + + @Test + @DisplayName("Commence should set forbidden status when bpn not found exception") + void commence_ShouldSetForbiddenStatus_WhenBpnNotFoundException() { + AuthenticationException authException = new AuthenticationException(StringPool.BPN_NOT_FOUND) { + }; + + customAuthenticationEntryPoint.commence(request, response, authException); + + verify(response).setStatus(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("Commence should set custom realm when realm name is set") + void commence_ShouldSetCustomRealm_WhenRealmNameIsSet() { + customAuthenticationEntryPoint.setRealmName("custom-realm"); + + OAuth2Error error = new OAuth2Error("invalid_token", "The token is invalid", "https://example.com"); + OAuth2AuthenticationException authException = new OAuth2AuthenticationException(error); + + customAuthenticationEntryPoint.commence(request, response, authException); + + ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(String.class); + verify(response).addHeader(eq(HttpHeaders.WWW_AUTHENTICATE), headerCaptor.capture()); + + String wwwAuthenticate = headerCaptor.getValue(); + assertEquals("Bearer realm=\"custom-realm\", error=\"invalid_token\", error_description=\"The token is invalid\", error_uri=\"https://example.com\"", wwwAuthenticate); + } + + @Test + @DisplayName("Commence should set scope when bearer token error has scope") + void commence_ShouldSetScope_WhenBearerTokenErrorHasScope() { + BearerTokenError error = new BearerTokenError("insufficient_scope", HttpStatus.UNAUTHORIZED, "Insufficient scope", "https://example.com", "scope1 scope2"); + OAuth2AuthenticationException authException = new OAuth2AuthenticationException(error); + + customAuthenticationEntryPoint.commence(request, response, authException); + + ArgumentCaptor headerCaptor = ArgumentCaptor.forClass(String.class); + verify(response).addHeader(eq(HttpHeaders.WWW_AUTHENTICATE), headerCaptor.capture()); + + String wwwAuthenticate = headerCaptor.getValue(); + assertEquals("Bearer error=\"insufficient_scope\", error_description=\"Insufficient scope\", error_uri=\"https://example.com\", scope=\"scope1 scope2\"", wwwAuthenticate); + } +} diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java new file mode 100644 index 00000000..794a6aea --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java @@ -0,0 +1,89 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.utils; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +class BpnValidatorTest { + + private BpnValidator bpnValidator; + private Jwt jwt; + + @BeforeEach + void setUp() { + bpnValidator = new BpnValidator(); + jwt = Mockito.mock(Jwt.class); + } + + @Test + @DisplayName("Validate when bpn claim is present") + void validateWhenBpnClaimIsPresent() { + Map claims = new HashMap<>(); + claims.put(StringPool.BPN, "123456"); + + when(jwt.getClaims()).thenReturn(claims); + + OAuth2TokenValidatorResult result = bpnValidator.validate(jwt); + + assertFalse(result.hasErrors()); + } + + @Test + @DisplayName("Validate when bpn claim is not present") + void validateWhenBpnClaimIsNotPresent() { + Map claims = new HashMap<>(); + + when(jwt.getClaims()).thenReturn(claims); + + OAuth2TokenValidatorResult result = bpnValidator.validate(jwt); + + assertTrue(result.hasErrors()); + assertEquals(bpnValidator.error.getErrorCode(), result.getErrors().iterator().next().getErrorCode()); + assertEquals(bpnValidator.error.getDescription(), result.getErrors().iterator().next().getDescription()); + } + + @Test + @DisplayName("Validate when bpn claim is present with different case") + void validateWhenBpnClaimIsPresentWithDifferentCase() { + Map claims = new HashMap<>(); + claims.put("BPN", "123456"); + + when(jwt.getClaims()).thenReturn(claims); + + OAuth2TokenValidatorResult result = bpnValidator.validate(jwt); + + assertFalse(result.hasErrors()); + } +} From a4fa6cc37d72e57796616fd87716fef059770e76 Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Tue, 23 Jul 2024 19:34:06 +0530 Subject: [PATCH 10/60] feat: test cases added, file header updated and detail log added for security events --- .../config/security/SecurityEvents.java | 6 +- .../controller/DidDocumentController.java | 2 +- .../controller/WalletController.java | 2 +- .../utils/TokenParsingUtils.java | 67 +++++ .../utils/TokenParsingUtilsTest.java | 228 ++++++++++++++++++ 5 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityEvents.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityEvents.java index 841bd3fd..90dc1f86 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityEvents.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityEvents.java @@ -38,7 +38,11 @@ public void onFailure(AbstractAuthenticationFailureEvent failures) { @EventListener public void onFailure(AuthorizationDeniedEvent failure) { - log.warn("Failed Authorization: Missing 'Authorization' header."); + if (failure.getAuthorizationDecision() != null) { + log.warn("Failed Authorization: {}",failure.getAuthorizationDecision().toString()); + } else { + log.warn("Failed Authorization: Missing 'Authorization' header."); + } } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java index 298fdc10..7c3a8ad8 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/DidDocumentController.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java index d38e353e..4b1ded21 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java index 88b6bddb..7e6d3cad 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java @@ -41,13 +41,31 @@ import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; +/** + * The type Token parsing utils. + */ @UtilityClass public class TokenParsingUtils { + /** + * The constant PARSING_TOKEN_ERROR. + */ public static final String PARSING_TOKEN_ERROR = "Could not parse jwt token"; + /** + * The constant BEARER_ACCESS_SCOPE. + */ public static final String BEARER_ACCESS_SCOPE = "bearer_access_scope"; + /** + * The constant ACCESS_TOKEN_ERROR. + */ public static final String ACCESS_TOKEN_ERROR = "Access token not present"; + /** + * Gets claims set. + * + * @param tokenParsed the token parsed + * @return the claims set + */ public static JWTClaimsSet getClaimsSet(SignedJWT tokenParsed) { try { return tokenParsed.getJWTClaimsSet(); @@ -56,6 +74,12 @@ public static JWTClaimsSet getClaimsSet(SignedJWT tokenParsed) { } } + /** + * Parse token signed jwt. + * + * @param token the token + * @return the signed jwt + */ public static SignedJWT parseToken(String token) { try { return SignedJWT.parse(token); @@ -64,6 +88,13 @@ public static SignedJWT parseToken(String token) { } } + /** + * Gets string claim. + * + * @param claimsSet the claims set + * @param name the name + * @return the string claim + */ public static String getStringClaim(JWTClaimsSet claimsSet, String name) { try { return claimsSet.getStringClaim(name); @@ -72,6 +103,12 @@ public static String getStringClaim(JWTClaimsSet claimsSet, String name) { } } + /** + * Gets access token. + * + * @param claims the claims + * @return the access token + */ public static Optional getAccessToken(JWTClaimsSet claims) { try { String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN); @@ -81,6 +118,12 @@ public static Optional getAccessToken(JWTClaimsSet claims) { } } + /** + * Gets access token. + * + * @param outerToken the outer token + * @return the access token + */ public static SignedJWT getAccessToken(String outerToken) { SignedJWT jwtOuter = parseToken(outerToken); JWTClaimsSet claimsSet = getClaimsSet(jwtOuter); @@ -88,6 +131,12 @@ public static SignedJWT getAccessToken(String outerToken) { return accessToken.map(TokenParsingUtils::parseToken).orElseThrow(() -> new BadDataException(ACCESS_TOKEN_ERROR)); } + /** + * Gets scope. + * + * @param jwtClaimsSet the jwt claims set + * @return the scope + */ public static String getScope(JWTClaimsSet jwtClaimsSet) { try { String scopes = jwtClaimsSet.getStringClaim(SCOPE); @@ -100,6 +149,12 @@ public static String getScope(JWTClaimsSet jwtClaimsSet) { } } + /** + * Gets jti access token. + * + * @param accessToken the access token + * @return the jti access token + */ public static String getJtiAccessToken(JWT accessToken) { try { return getStringClaim(accessToken.getJWTClaimsSet(), JTI); @@ -108,6 +163,12 @@ public static String getJtiAccessToken(JWT accessToken) { } } + /** + * Gets nonce access token. + * + * @param accessToken the access token + * @return the nonce access token + */ public static String getNonceAccessToken(JWT accessToken) { try { return accessToken.getJWTClaimsSet().getStringClaim(NONCE); @@ -116,6 +177,12 @@ public static String getNonceAccessToken(JWT accessToken) { } } + /** + * Gets bpn from token. + * + * @param authentication the authentication + * @return the bpn from token + */ public static String getBPNFromToken(Authentication authentication) { Jwt jwt = ((JwtAuthenticationToken) authentication).getToken(); // this will misbehave if we have more then one claims with different case diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java new file mode 100644 index 00000000..858b2677 --- /dev/null +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java @@ -0,0 +1,228 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.utils; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.text.ParseException; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class TokenParsingUtilsTest { + + @Test + void parseTokenShouldReturnSignedJWTWhenTokenIsValid() throws ParseException { + String token = "valid.token.here"; + SignedJWT signedJWT = mock(SignedJWT.class); + + try (MockedStatic mockedSignedJWT = mockStatic(SignedJWT.class)) { + mockedSignedJWT.when(() -> SignedJWT.parse(token)).thenReturn(signedJWT); + + SignedJWT result = TokenParsingUtils.parseToken(token); + + assertEquals(signedJWT, result); + } + } + + @Test + void parseTokenShouldThrowBadDataExceptionWhenParseExceptionOccurs() throws ParseException { + String token = "invalid.token.here"; + + try (MockedStatic mockedSignedJWT = mockStatic(SignedJWT.class)) { + mockedSignedJWT.when(() -> SignedJWT.parse(token)).thenThrow(ParseException.class); + + BadDataException exception = assertThrows(BadDataException.class, () -> TokenParsingUtils.parseToken(token)); + + assertEquals(TokenParsingUtils.PARSING_TOKEN_ERROR, exception.getMessage()); + } + } + + @Test + void getAccessTokenShouldReturnInnerSignedJWTWhenAccessTokenIsPresent() throws ParseException { + String outerToken = "outer.token.here"; + SignedJWT outerSignedJWT = mock(SignedJWT.class); + JWTClaimsSet outerClaimsSet = new JWTClaimsSet.Builder().claim("access_token", "inner.token.here").build(); + SignedJWT innerSignedJWT = mock(SignedJWT.class); + + try (MockedStatic mockedSignedJWT = mockStatic(SignedJWT.class)) { + mockedSignedJWT.when(() -> SignedJWT.parse(outerToken)).thenReturn(outerSignedJWT); + mockedSignedJWT.when(() -> SignedJWT.parse("inner.token.here")).thenReturn(innerSignedJWT); + when(outerSignedJWT.getJWTClaimsSet()).thenReturn(outerClaimsSet); + + SignedJWT result = TokenParsingUtils.getAccessToken(outerToken); + + assertEquals(innerSignedJWT, result); + } + } + + @Test + void getAccessTokenShouldThrowBadDataExceptionWhenAccessTokenIsNotPresent() throws ParseException { + String outerToken = "outer.token.here"; + SignedJWT outerSignedJWT = mock(SignedJWT.class); + JWTClaimsSet outerClaimsSet = new JWTClaimsSet.Builder().build(); + + try (MockedStatic mockedSignedJWT = mockStatic(SignedJWT.class)) { + mockedSignedJWT.when(() -> SignedJWT.parse(outerToken)).thenReturn(outerSignedJWT); + when(outerSignedJWT.getJWTClaimsSet()).thenReturn(outerClaimsSet); + + BadDataException exception = assertThrows(BadDataException.class, () -> TokenParsingUtils.getAccessToken(outerToken)); + + assertEquals(TokenParsingUtils.ACCESS_TOKEN_ERROR, exception.getMessage()); + } + } + + @Test + void getBPNFromTokenShouldReturnBPNWhenBPNClaimIsPresent() { + Authentication authentication = mock(JwtAuthenticationToken.class); + Jwt jwt = mock(Jwt.class); + when(((JwtAuthenticationToken) authentication).getToken()).thenReturn(jwt); + Map claims = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + claims.put(StringPool.BPN, "123456"); + when(jwt.getClaims()).thenReturn(claims); + + String result = TokenParsingUtils.getBPNFromToken(authentication); + + assertEquals("123456", result); + } + + // Other test methods for TokenParsingUtils... + + @Test + void getStringClaimShouldReturnClaimValueWhenClaimIsPresent() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("claim")).thenReturn("value"); + + String result = TokenParsingUtils.getStringClaim(claimsSet, "claim"); + + assertEquals("value", result); + } + + @Test + void getStringClaimShouldThrowBadDataExceptionWhenParseExceptionOccurs() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("claim")).thenThrow(ParseException.class); + + BadDataException exception = assertThrows(BadDataException.class, () -> TokenParsingUtils.getStringClaim(claimsSet, "claim")); + + assertEquals(TokenParsingUtils.PARSING_TOKEN_ERROR, exception.getMessage()); + } + + @Test + void getAccessTokenShouldReturnAccessTokenWhenAccessTokenIsPresent() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("access_token")).thenReturn("accessToken"); + + Optional result = TokenParsingUtils.getAccessToken(claimsSet); + + assertTrue(result.isPresent()); + assertEquals("accessToken", result.get()); + } + + @Test + void getAccessTokenShouldReturnEmptyOptionalWhenAccessTokenIsNotPresent() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("access_token")).thenReturn(null); + + Optional result = TokenParsingUtils.getAccessToken(claimsSet); + + assertFalse(result.isPresent()); + } + + @Test + void getAccessTokenShouldThrowBadDataExceptionWhenParseExceptionOccurs() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("access_token")).thenThrow(ParseException.class); + + BadDataException exception = assertThrows(BadDataException.class, () -> TokenParsingUtils.getAccessToken(claimsSet)); + + assertEquals(TokenParsingUtils.PARSING_TOKEN_ERROR, exception.getMessage()); + } + + @Test + void getScopeShouldReturnScopeWhenScopeIsPresent() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("scope")).thenReturn("scope1 scope2"); + + String result = TokenParsingUtils.getScope(claimsSet); + + assertEquals("scope1 scope2", result); + } + + @Test + void getScopeShouldReturnBearerAccessScopeWhenScopeIsNotPresentButBearerAccessScopeIs() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("scope")).thenReturn(null); + when(claimsSet.getStringClaim(TokenParsingUtils.BEARER_ACCESS_SCOPE)).thenReturn("bearerAccessScope"); + + String result = TokenParsingUtils.getScope(claimsSet); + + assertEquals("bearerAccessScope", result); + } + + @Test + void getScopeShouldThrowBadDataExceptionWhenParseExceptionOccurs() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim("scope")).thenThrow(ParseException.class); + + BadDataException exception = assertThrows(BadDataException.class, () -> TokenParsingUtils.getScope(claimsSet)); + + assertEquals("Token does not contain scope claim", exception.getMessage()); + } + + @Test + void getJtiAccessTokenShouldReturnJtiWhenClaimIsPresent() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim(JwtClaimNames.JTI)).thenReturn("jtiValue"); + SignedJWT signedJWT = mock(SignedJWT.class); + when(signedJWT.getJWTClaimsSet()).thenReturn(claimsSet); + + String result = TokenParsingUtils.getJtiAccessToken(signedJWT); + + assertEquals("jtiValue", result); + } + + @Test + void getJtiAccessTokenShouldThrowBadDataExceptionWhenParseExceptionOccurs() throws ParseException { + JWTClaimsSet claimsSet = mock(JWTClaimsSet.class); + when(claimsSet.getStringClaim(JwtClaimNames.JTI)).thenThrow(ParseException.class); + SignedJWT signedJWT = mock(SignedJWT.class); + when(signedJWT.getJWTClaimsSet()).thenReturn(claimsSet); + + BadDataException exception = assertThrows(BadDataException.class, () -> TokenParsingUtils.getJtiAccessToken(signedJWT)); + + assertEquals(TokenParsingUtils.PARSING_TOKEN_ERROR, exception.getMessage()); + } + +} From ae2710f8cbadf28eb263ca40eb61dbc3fa4dfcaf Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 29 Jul 2024 09:20:43 +0000 Subject: [PATCH 11/60] chore(release): 1.0.0-develop.3 [skip ci] # [1.0.0-develop.3](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.2...v1.0.0-develop.3) (2024-07-29) ### Features * remove BaseController, change Principal to Authenticationand unit test cases added ([15425be](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/15425beccd8bbb3560328d7d845766f422e6e4d8)) * test cases added, file header updated and detail log added for security events ([a4fa6cc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/a4fa6cc37d72e57796616fd87716fef059770e76)) --- CHANGELOG.md | 8 ++++++++ charts/managed-identity-wallet/Chart.yaml | 4 ++-- charts/managed-identity-wallet/README.md | 2 +- gradle.properties | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d95b45..150875ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [1.0.0-develop.3](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.2...v1.0.0-develop.3) (2024-07-29) + + +### Features + +* remove BaseController, change Principal to Authenticationand unit test cases added ([15425be](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/15425beccd8bbb3560328d7d845766f422e6e4d8)) +* test cases added, file header updated and detail log added for security events ([a4fa6cc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/a4fa6cc37d72e57796616fd87716fef059770e76)) + # [1.0.0-develop.2](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.1...v1.0.0-develop.2) (2024-07-22) diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index cb8af8d5..3807b96c 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -25,8 +25,8 @@ description: | type: application -version: 1.0.0-develop.2 -appVersion: 1.0.0-develop.2 +version: 1.0.0-develop.3 +appVersion: 1.0.0-develop.3 home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 8e004f9e..afce25da 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -2,7 +2,7 @@ # managed-identity-wallet -![Version: 1.0.0-develop.2](https://img.shields.io/badge/Version-1.0.0--develop.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.2](https://img.shields.io/badge/AppVersion-1.0.0--develop.2-informational?style=flat-square) +![Version: 1.0.0-develop.3](https://img.shields.io/badge/Version-1.0.0--develop.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.3](https://img.shields.io/badge/AppVersion-1.0.0--develop.3-informational?style=flat-square) Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. diff --git a/gradle.properties b/gradle.properties index fe8ac71d..d00038e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ jacocoVersion=0.8.9 springBootVersion=3.3.2 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=1.0.0-develop.2 +applicationVersion=1.0.0-develop.3 openApiVersion=2.1.0 From d470f86816b8e534a8a2a64548d8e42812776438 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 25 Jul 2024 16:02:27 +0200 Subject: [PATCH 12/60] chore(release): 1.0.0-develop.3 [skip ci] # [1.0.0-develop.3](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.2...v1.0.0-develop.3) (2024-07-29) ### Features * remove BaseController, change Principal to Authenticationand unit test cases added ([15425be](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/15425beccd8bbb3560328d7d845766f422e6e4d8)) * test cases added, file header updated and detail log added for security events ([a4fa6cc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/a4fa6cc37d72e57796616fd87716fef059770e76)) Signed-off-by: Dominik Pinsel --- .../security/PresentationIatpFilter.java | 83 ------------------- .../config/security/SecurityConfig.java | 12 ++- .../controller/PresentationController.java | 34 ++++++-- .../PresentationIatpFilterTest.java | 7 +- .../identityminustrust/TokenRequestTest.java | 2 +- 5 files changed, 41 insertions(+), 97 deletions(-) delete mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java deleted file mode 100644 index cefefb8f..00000000 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * ******************************************************************************* - * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ****************************************************************************** - */ - -package org.eclipse.tractusx.managedidentitywallets.config.security; - -import io.micrometer.common.util.StringUtils; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; -import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.web.filter.GenericFilterBean; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COMA_SEPARATOR; - -public class PresentationIatpFilter extends GenericFilterBean { - - RequestMatcher customFilterUrl1 = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP); - RequestMatcher customFilterUrl2 = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP_WORKAROUND); - - STSTokenValidationService validationService; - - public PresentationIatpFilter(STSTokenValidationService validationService) { - this.validationService = validationService; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - - if (customFilterUrl1.matches(httpServletRequest) || customFilterUrl2.matches(httpServletRequest)) { - String authHeader = httpServletRequest.getHeader("Authorization"); - if (StringUtils.isEmpty(authHeader)) { - httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } else { - ValidationResult result = validationService.validateToken(authHeader); - if (!result.isValid()) { - List errorValues = new ArrayList<>(); - result.getErrors().forEach(c -> errorValues.add(c.name())); - String content = String.join(COMA_SEPARATOR, errorValues); - - httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - httpServletResponse.setContentLength(content.length()); - httpServletResponse.getWriter().write(content); - } else { - chain.doFilter(request, response); - } - } - } else { - chain.doFilter(request, response); - } - } -} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index 937060e1..a0a6178e 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -48,8 +48,11 @@ import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; + +import java.util.List; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; @@ -127,8 +130,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() ).oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(securityConfigProperties.clientId()))) - .authenticationEntryPoint(new CustomAuthenticationEntryPoint())) - .addFilterAfter(new PresentationIatpFilter(validationService), BasicAuthenticationFilter.class); + .authenticationEntryPoint(new CustomAuthenticationEntryPoint())) + .securityMatcher(new NegatedRequestMatcher(new OrRequestMatcher( + List.of( + new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP), + new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP_WORKAROUND))))); return http.build(); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index 31567822..cb38bca5 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -23,6 +23,7 @@ import com.nimbusds.jwt.SignedJWT; import io.swagger.v3.oas.annotations.Parameter; +import liquibase.util.StringUtil; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -33,6 +34,7 @@ import org.eclipse.tractusx.managedidentitywallets.dto.PresentationResponseMessage; import org.eclipse.tractusx.managedidentitywallets.reader.TractusXPresentationRequestReader; import org.eclipse.tractusx.managedidentitywallets.service.PresentationService; +import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; import org.springframework.http.HttpStatus; @@ -63,6 +65,8 @@ public class PresentationController { private final TractusXPresentationRequestReader presentationRequestReader; + private final STSTokenValidationService validationService; + /** * Create presentation response entity. * @@ -114,17 +118,37 @@ public ResponseEntity> validatePresentation(@RequestBody Map @PostMapping(path = { RestURI.API_PRESENTATIONS_IATP, RestURI.API_PRESENTATIONS_IATP_WORKAROUND }, produces = { MediaType.APPLICATION_JSON_VALUE }) @GetVerifiablePresentationIATPApiDocs @SneakyThrows - public ResponseEntity createPresentation(@Parameter(hidden = true) @RequestHeader(name = "Authorization") String stsToken, - @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, - InputStream is) { + public ResponseEntity createPresentation( + /* As filters are disabled for this endpoint set required to false and handle missing token manually */ + @Parameter(hidden = true) @RequestHeader(name = "Authorization", required = false) String stsToken, + @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, + InputStream is) { try { + if(stsToken == null){ + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + if (stsToken.startsWith("Bearer ")) { + stsToken = stsToken.substring("Bearer ".length()); + } + + var validationResult = validationService.validateToken(stsToken); + if (!validationResult.isValid()) { + log.atDebug().log("Unauthorized request. Errors: '%s'".formatted( + StringUtil.join(validationResult.getErrors().stream() + .map(Enum::name) + .toList(), + ", "))); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + // requested scopes are ignored for now final List requestedScopes = presentationRequestReader.readVerifiableCredentialScopes(is); - // requested scopes are ignored until the documentation is better refined SignedJWT accessToken = getAccessToken(stsToken); Map map = presentationService.createVpWithRequiredScopes(accessToken, asJwt); - VerifiablePresentation verifiablePresentation = new VerifiablePresentation((Map)map.get("vp")); + VerifiablePresentation verifiablePresentation = new VerifiablePresentation((Map) map.get("vp")); PresentationResponseMessage message = new PresentationResponseMessage(verifiablePresentation); return ResponseEntity.ok(message); } catch (TractusXPresentationRequestReader.InvalidPresentationQueryMessageResource e) { diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java index ed2555bd..d85994f1 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java @@ -70,7 +70,7 @@ void createPresentationFailure401Test() { HttpEntity entity = new HttpEntity<>(headers); ResponseEntity> response = testTemplate.exchange( RestURI.API_PRESENTATIONS_IATP, - HttpMethod.GET, + HttpMethod.POST, entity, new ParameterizedTypeReference<>() { } @@ -94,15 +94,12 @@ void createPresentationFailure401WithErrorsTest() { ResponseEntity response = testTemplate.exchange( RestURI.API_PRESENTATIONS_IATP, - HttpMethod.GET, + HttpMethod.POST, entity, new ParameterizedTypeReference<>() { } ); - String expectedBody = TOKEN_ALREADY_EXPIRED.name() + StringPool.COMA_SEPARATOR + NONCE_MISSING.name(); - Assertions.assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); - Assertions.assertEquals(expectedBody, response.getBody()); } } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java index 44317ee0..0bbfeeec 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java @@ -148,7 +148,7 @@ public void testPresentationQueryWithToken() { final Map data2 = MAPPER.readValue(message2, Map.class); final HttpHeaders headers2 = new HttpHeaders(); - headers2.set(HttpHeaders.AUTHORIZATION, jwt); + headers2.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt); final HttpEntity> entity2 = new HttpEntity<>(data2, headers2); var result2 = restTemplate .postForEntity(RestURI.API_PRESENTATIONS_IATP, entity2, String.class); From 233ab6883012a5523ecaa4e0ad234e775c3e4577 Mon Sep 17 00:00:00 2001 From: Dominik-Pinsel Date: Fri, 9 Aug 2024 11:36:09 +0200 Subject: [PATCH 13/60] feat(API)!: change API VC/VP default data format from Json to JWT BREAKING CHANGE: All API endpoints that used Verifiable Credentials and -Presentations in JSON format per default are now working with the JWT format by default instead. Signed-off-by: Dominik Pinsel --- .../HoldersCredentialController.java | 4 ++-- .../IssuersCredentialController.java | 4 ++-- .../controller/PresentationController.java | 23 +++++++++++++------ .../dto/PresentationResponseMessage.java | 12 ++++++---- .../utils/TestUtils.java | 2 +- .../vc/HoldersCredentialTest.java | 18 +++++++-------- .../vc/IssuersCredentialTest.java | 16 ++++++------- .../vp/PresentationTest.java | 6 ++--- 8 files changed, 48 insertions(+), 37 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java index e037506f..8d85d87d 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java @@ -93,7 +93,7 @@ public ResponseEntity> getCredentials(@Parameter(n @Parameter(name = "sortTpe", description = "Sort order", examples = {@ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order")}) @RequestParam(required = false, defaultValue = "desc") String sortTpe, @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Page number, Page number start with zero") @RequestParam(required = false, defaultValue = "0") int pageNumber, @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, - @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt, + @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt, Authentication authentication) { log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); @@ -124,7 +124,7 @@ public ResponseEntity> getCredentials(@Parameter(n @IssueCredentialApiDoc @PostMapping(path = RestURI.CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity issueCredential(@RequestBody Map data, Authentication authentication, - @AsJwtParam @RequestParam(name = "asJwt", defaultValue = "false") boolean asJwt + @AsJwtParam @RequestParam(name = "asJwt", defaultValue = "true") boolean asJwt ) { log.debug("Received request to issue credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, TokenParsingUtils.getBPNFromToken(authentication), asJwt)); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java index 9ade312f..55592d09 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java @@ -101,7 +101,7 @@ public ResponseEntity> getCredentials(@Parameter(n } ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, @Parameter(name = "sortTpe", description = "Sort order", examples = { @ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order") }) @RequestParam(required = false, defaultValue = "desc") String sortTpe, - @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt, + @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt, Authentication authentication) { log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); final GetCredentialsCommand command; @@ -146,7 +146,7 @@ public ResponseEntity> credentialsValidation(@RequestBody Cr @PostMapping(path = RestURI.ISSUERS_CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @IssueVerifiableCredentialUsingBaseWalletApiDocs public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = {@ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Authentication authentication, - @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "false") boolean asJwt) { + @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt) { log.debug("Received request to issue verifiable credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, TokenParsingUtils.getBPNFromToken(authentication))); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index cb38bca5..f7b06271 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -80,7 +80,7 @@ public class PresentationController { @PostMapping(path = RestURI.API_PRESENTATIONS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> createPresentation(@RequestBody Map data, @RequestParam(name = "audience", required = false) String audience, - @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, Authentication authentication + @RequestParam(name = "asJwt", required = false, defaultValue = "true") boolean asJwt, Authentication authentication ) { log.debug("Received request to create presentation. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); return ResponseEntity.status(HttpStatus.CREATED).body(presentationService.createPresentation(data, asJwt, audience, TokenParsingUtils.getBPNFromToken(authentication))); @@ -99,7 +99,7 @@ public ResponseEntity> createPresentation(@RequestBody Map> validatePresentation(@RequestBody Map data, @Parameter(description = "Audience to validate in VP (Only supported in case of JWT formatted VP)") @RequestParam(name = "audience", required = false) String audience, - @Parameter(description = "Pass true in case of VP is in JWT format") @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, + @Parameter(description = "Pass true in case of VP is in JWT format") @RequestParam(name = "asJwt", required = false, defaultValue = "true") boolean asJwt, @Parameter(description = "Check expiry of VC(Only supported in case of JWT formatted VP)") @RequestParam(name = "withCredentialExpiryDate", required = false, defaultValue = "false") boolean withCredentialExpiryDate ) { log.debug("Received request to validate presentation"); @@ -121,7 +121,7 @@ public ResponseEntity> validatePresentation(@RequestBody Map public ResponseEntity createPresentation( /* As filters are disabled for this endpoint set required to false and handle missing token manually */ @Parameter(hidden = true) @RequestHeader(name = "Authorization", required = false) String stsToken, - @RequestParam(name = "asJwt", required = false, defaultValue = "false") boolean asJwt, + @RequestParam(name = "asJwt", required = false, defaultValue = "true") boolean asJwt, InputStream is) { try { @@ -147,10 +147,19 @@ public ResponseEntity createPresentation( final List requestedScopes = presentationRequestReader.readVerifiableCredentialScopes(is); SignedJWT accessToken = getAccessToken(stsToken); - Map map = presentationService.createVpWithRequiredScopes(accessToken, asJwt); - VerifiablePresentation verifiablePresentation = new VerifiablePresentation((Map) map.get("vp")); - PresentationResponseMessage message = new PresentationResponseMessage(verifiablePresentation); - return ResponseEntity.ok(message); + + if(asJwt) { + Map map = presentationService.createVpWithRequiredScopes(accessToken, true); + String verifiablePresentation = (String) map.get("vp"); + PresentationResponseMessage message = new PresentationResponseMessage(verifiablePresentation); + return ResponseEntity.ok(message); + } else { + Map map = presentationService.createVpWithRequiredScopes(accessToken, false); + VerifiablePresentation verifiablePresentation = new VerifiablePresentation((Map) map.get("vp")); + PresentationResponseMessage message = new PresentationResponseMessage(verifiablePresentation); + return ResponseEntity.ok(message); + } + } catch (TractusXPresentationRequestReader.InvalidPresentationQueryMessageResource e) { return ResponseEntity.badRequest().build(); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java index 5636f3cf..33326fbc 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/PresentationResponseMessage.java @@ -34,25 +34,27 @@ * As `presentationSubmission` a not well-defined, we will just skip the property for HTTP responses. Defining all types as 'Json' make the whole idea of using Json-Linked-Data a waste of time, but ok. *

* The `presentation` property is only specified as 'Json'. For this implementation we will assume these are Presentations from ether the Verifiable Credential Data Model v1.1 or Verifiable Credential Data Model v2.0. + *
+ * At the same time other applications require the Verifiable Presentation to be a Json Web Token. As this protocol is not able to define a good data type, and implementations of this protocol even require different types, object is the correct data type here... */ @Getter public class PresentationResponseMessage { - public PresentationResponseMessage(VerifiablePresentation verifiablePresentation) { + public PresentationResponseMessage(Object verifiablePresentation) { this(List.of(verifiablePresentation)); } - public PresentationResponseMessage(List verifiablePresentations) { + public PresentationResponseMessage(List verifiablePresentations) { this.verifiablePresentations = verifiablePresentations; } @JsonProperty("@context") - private List contexts = List.of("https://w3id.org/tractusx-trust/v0.8"); + private List contexts = List.of("https://w3id.org/tractusx-trust/v0.8"); @JsonProperty("@type") - private List types = List.of("PresentationResponseMessage"); + private List types = List.of("PresentationResponseMessage"); @JsonProperty("presentation") - private List verifiablePresentations; + private List verifiablePresentations; } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 0304ed18..005efcea 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -229,7 +229,7 @@ public static VerifiableCredential issueCustomVCUsingBaseWallet(String holderBPn Map map = getCredentialAsMap(holderBPn, holderDid, issuerDid, type, miwSettings, objectMapper); HttpEntity entity = new HttpEntity<>(map, headers); - ResponseEntity response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?holderDid={did}", HttpMethod.POST, entity, String.class, holderDid); + ResponseEntity response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?holderDid={did}&asJwt={asJwt}", HttpMethod.POST, entity, String.class, holderDid, false); if (response.getStatusCode().value() == HttpStatus.FORBIDDEN.value()) { throw new ForbiddenException(); } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java index dcd0344e..7da209a7 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java @@ -170,20 +170,20 @@ void getCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcepti HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange(RestURI.CREDENTIALS + "?issuerIdentifier={did}" - , HttpMethod.GET, entity, String.class, baseDID); + ResponseEntity response = restTemplate.exchange(RestURI.CREDENTIALS + "?issuerIdentifier={did}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, baseDID, false); List credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); Assertions.assertEquals(typesOfVcs.size(), Objects.requireNonNull(credentialList).size()); - response = restTemplate.exchange(RestURI.CREDENTIALS + "?credentialId={id}" - , HttpMethod.GET, entity, String.class, credentialList.get(0).getId()); + response = restTemplate.exchange(RestURI.CREDENTIALS + "?credentialId={id}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, credentialList.get(0).getId(), false); credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); Assertions.assertEquals(1, Objects.requireNonNull(credentialList).size()); - response = restTemplate.exchange(RestURI.CREDENTIALS + "?type={list}" - , HttpMethod.GET, entity, String.class, String.join(",", typesOfVcs)); + response = restTemplate.exchange(RestURI.CREDENTIALS + "?type={list}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, String.join(",", typesOfVcs), false); credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); Assertions.assertEquals(typesOfVcs.size(), Objects.requireNonNull(credentialList).size()); @@ -191,8 +191,8 @@ void getCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcepti //test get by type String type = typesOfVcs.get(0); - response = restTemplate.exchange(RestURI.CREDENTIALS + "?type={list}" - , HttpMethod.GET, entity, String.class, type); + response = restTemplate.exchange(RestURI.CREDENTIALS + "?type={list}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, type, false); credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); Assertions.assertEquals(1, Objects.requireNonNull(credentialList).size()); @@ -379,7 +379,7 @@ private ResponseEntity issueVC(String bpn, String did, String type, Http Map map = objectMapper.readValue(credentialWithoutProof.toJson(), Map.class); HttpEntity entity = new HttpEntity<>(map, headers); - ResponseEntity response = restTemplate.exchange(RestURI.CREDENTIALS, HttpMethod.POST, entity, String.class); + ResponseEntity response = restTemplate.exchange(RestURI.CREDENTIALS+ "?asJwt={asJwt}", HttpMethod.POST, entity, String.class, false); return response; } } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java index 46f602d2..7f6473bc 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java @@ -100,8 +100,8 @@ void getCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcepti HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?holderIdentifier={did}" - , HttpMethod.GET, entity, String.class, holderDID); + ResponseEntity response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?holderIdentifier={did}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, holderDID, false); List credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); @@ -109,15 +109,15 @@ void getCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcepti Assertions.assertEquals(typesOfVcs.size(), Objects.requireNonNull(credentialList).size()); - response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?credentialId={id}" - , HttpMethod.GET, entity, String.class, credentialList.get(0).getId()); + response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?credentialId={id}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, credentialList.get(0).getId(), false); credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); Assertions.assertEquals(1, Objects.requireNonNull(credentialList).size()); - response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?type={list}&holderIdentifier={holderIdentifier}" - , HttpMethod.GET, entity, String.class, String.join(",", typesOfVcs), wallet.getBpn()); + response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?type={list}&holderIdentifier={holderIdentifier}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, String.join(",", typesOfVcs), wallet.getBpn(), false); credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); @@ -125,8 +125,8 @@ void getCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcepti Assertions.assertEquals(typesOfVcs.size(), Objects.requireNonNull(credentialList).size()); - response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?type={list}&holderIdentifier={holderIdentifier}" - , HttpMethod.GET, entity, String.class, typesOfVcs.get(0), wallet.getBpn()); + response = restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?type={list}&holderIdentifier={holderIdentifier}&asJwt={asJwt}" + , HttpMethod.GET, entity, String.class, typesOfVcs.get(0), wallet.getBpn(), false); credentialList = TestUtils.getVerifiableCredentials(response, objectMapper); Assertions.assertEquals(HttpStatus.OK.value(), response.getStatusCode().value()); Assertions.assertEquals(1, Objects.requireNonNull(credentialList).size()); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java index 6b9e55b2..f7495895 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java @@ -114,7 +114,7 @@ void validateVPAssJsonLd400() throws JsonProcessingException, JSONException { HttpHeaders headers = AuthenticationUtils.getValidUserHttpHeaders(bpn); HttpEntity entity = new HttpEntity<>(body, headers); - ResponseEntity validationResponse = restTemplate.exchange(RestURI.API_PRESENTATIONS_VALIDATION, HttpMethod.POST, entity, Map.class); + ResponseEntity validationResponse = restTemplate.exchange(RestURI.API_PRESENTATIONS_VALIDATION + "?asJwt={asJwt}", HttpMethod.POST, entity, Map.class, false); Assertions.assertEquals(validationResponse.getStatusCode().value(), HttpStatus.BAD_REQUEST.value()); } @@ -243,7 +243,7 @@ void createPresentationAsJsonLD201() throws JsonProcessingException, JSONExcepti HttpEntity entity = new HttpEntity<>(objectMapper.writeValueAsString(request), headers); - ResponseEntity vpResponse = restTemplate.exchange(RestURI.API_PRESENTATIONS, HttpMethod.POST, entity, Map.class); + ResponseEntity vpResponse = restTemplate.exchange(RestURI.API_PRESENTATIONS + "?asJwt={asJwt}", HttpMethod.POST, entity, Map.class, false); Assertions.assertEquals(vpResponse.getStatusCode().value(), HttpStatus.CREATED.value()); } @@ -347,6 +347,6 @@ private ResponseEntity issueVC(String bpn, String holderDid, String issu Map map = objectMapper.readValue(credentialWithoutProof.toJson(), Map.class); HttpEntity entity = new HttpEntity<>(map, headers); - return restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?holderDid={did}", HttpMethod.POST, entity, String.class, holderDid); + return restTemplate.exchange(RestURI.ISSUERS_CREDENTIALS + "?holderDid={did}&asJwt={asJwt}", HttpMethod.POST, entity, String.class, holderDid, false); } } From 2a29ef8ac9410e8fa248f6de0f96b87ad9576684 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 9 Aug 2024 09:49:58 +0000 Subject: [PATCH 14/60] chore(release): 1.0.0-develop.4 [skip ci] # [1.0.0-develop.4](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.3...v1.0.0-develop.4) (2024-08-09) * feat(API)!: change API VC/VP default data format from Json to JWT ([233ab68](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/233ab6883012a5523ecaa4e0ad234e775c3e4577)) ### BREAKING CHANGES * All API endpoints that used Verifiable Credentials and -Presentations in JSON format per default are now working with the JWT format by default instead. Signed-off-by: Dominik Pinsel --- CHANGELOG.md | 12 ++++++++++++ charts/managed-identity-wallet/Chart.yaml | 4 ++-- charts/managed-identity-wallet/README.md | 2 +- gradle.properties | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150875ec..b83e8bf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [1.0.0-develop.4](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.3...v1.0.0-develop.4) (2024-08-09) + + +* feat(API)!: change API VC/VP default data format from Json to JWT ([233ab68](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/233ab6883012a5523ecaa4e0ad234e775c3e4577)) + + +### BREAKING CHANGES + +* All API endpoints that used Verifiable Credentials and -Presentations in JSON format per default are now working with the JWT format by default instead. + +Signed-off-by: Dominik Pinsel + # [1.0.0-develop.3](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.2...v1.0.0-develop.3) (2024-07-29) diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 3807b96c..78627ee4 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -25,8 +25,8 @@ description: | type: application -version: 1.0.0-develop.3 -appVersion: 1.0.0-develop.3 +version: 1.0.0-develop.4 +appVersion: 1.0.0-develop.4 home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index afce25da..3984b37f 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -2,7 +2,7 @@ # managed-identity-wallet -![Version: 1.0.0-develop.3](https://img.shields.io/badge/Version-1.0.0--develop.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.3](https://img.shields.io/badge/AppVersion-1.0.0--develop.3-informational?style=flat-square) +![Version: 1.0.0-develop.4](https://img.shields.io/badge/Version-1.0.0--develop.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.4](https://img.shields.io/badge/AppVersion-1.0.0--develop.4-informational?style=flat-square) Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. diff --git a/gradle.properties b/gradle.properties index d00038e8..903e4072 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ jacocoVersion=0.8.9 springBootVersion=3.3.2 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=1.0.0-develop.3 +applicationVersion=1.0.0-develop.4 openApiVersion=2.1.0 From c173bd4d9902d798d75683247d13c89dda6861d2 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 13 Jun 2024 11:41:48 +0530 Subject: [PATCH 15/60] feat: intial revocation service added --- miw/DEPENDENCIES => DEPENDENCIES | 43 +- README.md | 367 +--------------- build.gradle | 152 +++++++ docs/api/{ => miw}/openapi_v001.json | 2 +- gradle.properties | 10 +- Dockerfile => miw/Dockerfile | 4 +- miw/README.md | 373 ++++++++++++++++ miw/build.gradle | 132 +----- revocation-service/Dockerfile | 15 + revocation-service/README.md | 130 ++++++ revocation-service/build.gradle | 134 ++++++ ...edentialsRevocationServiceApplication.java | 35 ++ .../RevocationApiControllerApiDocs.java | 199 +++++++++ .../revocation/config/ApplicationConfig.java | 51 +++ .../revocation/config/ExceptionHandling.java | 67 +++ .../revocation/config/MIWSettings.java | 40 ++ .../revocation/config/OpenApiConfig.java | 106 +++++ .../CustomAuthenticationConverter.java | 78 ++++ .../config/security/SecurityConfig.java | 119 +++++ .../security/SecurityConfigProperties.java | 37 ++ .../revocation/constant/ApplicationRole.java | 33 ++ .../revocation/constant/PurposeType.java | 27 ++ .../constant/RevocationApiEndpoints.java | 37 ++ .../controllers/BaseController.java | 52 +++ .../controllers/RevocationApiController.java | 136 ++++++ .../revocation/domain/BPN.java | 62 +++ .../revocation/dto/CredentialStatusDto.java | 48 ++ .../revocation/dto/StatusEntryDto.java | 41 ++ .../dto/StatusListCredentialSubject.java | 42 ++ .../revocation/dto/TokenResponse.java | 34 ++ .../exception/BitSetManagerException.java | 45 ++ .../CredentialAlreadyRevokedException.java | 29 ++ .../exception/ForbiddenException.java | 59 +++ .../exception/RevocationServiceException.java | 62 +++ .../revocation/jpa/StatusListCredential.java | 95 ++++ .../revocation/jpa/StatusListIndex.java | 79 ++++ .../jpa/StringToCredentialConverter.java | 52 +++ .../StatusListCredentialRepository.java | 38 ++ .../repository/StatusListIndexRepository.java | 37 ++ .../services/HttpClientService.java | 90 ++++ .../services/RevocationService.java | 365 ++++++++++++++++ .../revocation/utils/BitSetManager.java | 105 +++++ .../revocation/utils/StringPool.java | 27 ++ .../revocation/utils/Validate.java | 153 +++++++ .../validation/ValidVerifiableCredential.java | 43 ++ .../VerifiableCredentialValidator.java | 95 ++++ .../src/main/resources/application.yaml | 130 ++++++ .../db/changelog/changelog-master.xml | 28 ++ .../resources/db/changelog/changes/init.sql | 69 +++ .../revocation/TestUtil.java | 189 ++++++++ ...edentialsRevocationServiceApplication.java | 45 ++ .../config/ExceptionHandlingTest.java | 73 ++++ .../revocation/config/MIWSettingsTest.java | 55 +++ .../revocation/config/OpenApiConfigTest.java | 73 ++++ .../CustomAuthenticationConverterTest.java | 111 +++++ .../RevocationApiControllerTest.java | 223 ++++++++++ .../revocation/domain/BPNTest.java | 79 ++++ .../dto/CredentialStatusDtoTest.java | 154 +++++++ .../revocation/dto/StatusEntryDtoTest.java | 88 ++++ .../dto/StatusListCredentialSubjectTest.java | 144 ++++++ .../revocation/dto/TokenResponeTest.java | 70 +++ .../exception/BitSetManagerExceptionTest.java | 39 ++ .../RevocationServiceExceptionTest.java | 36 ++ .../jpa/StatusListCredentialTest.java | 159 +++++++ .../revocation/jpa/StatusListIndexTest.java | 165 +++++++ .../services/HttpClientServiceTest.java | 159 +++++++ .../services/RevocationServiceTest.java | 410 ++++++++++++++++++ .../revocation/utils/BitSetManagerTest.java | 109 +++++ .../revocation/utils/ValidateTest.java | 49 +++ settings.gradle | 1 + 70 files changed, 6139 insertions(+), 499 deletions(-) rename miw/DEPENDENCIES => DEPENDENCIES (88%) create mode 100644 build.gradle rename docs/api/{ => miw}/openapi_v001.json (99%) rename Dockerfile => miw/Dockerfile (88%) create mode 100644 miw/README.md create mode 100644 revocation-service/Dockerfile create mode 100644 revocation-service/README.md create mode 100644 revocation-service/build.gradle create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/VerifiableCredentialsRevocationServiceApplication.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ApplicationConfig.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettings.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfig.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverter.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPN.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponse.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerException.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/CredentialAlreadyRevokedException.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceException.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredential.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndex.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StringToCredentialConverter.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListCredentialRepository.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListIndexRepository.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManager.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/ValidVerifiableCredential.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java create mode 100644 revocation-service/src/main/resources/application.yaml create mode 100755 revocation-service/src/main/resources/db/changelog/changelog-master.xml create mode 100755 revocation-service/src/main/resources/db/changelog/changes/init.sql create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestVerifiableCredentialsRevocationServiceApplication.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfigTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerExceptionTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceExceptionTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredentialTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java diff --git a/miw/DEPENDENCIES b/DEPENDENCIES similarity index 88% rename from miw/DEPENDENCIES rename to DEPENDENCIES index 5b080ebb..5ef621c7 100644 --- a/miw/DEPENDENCIES +++ b/DEPENDENCIES @@ -19,8 +19,11 @@ maven/mavencentral/com.github.ben-manes.caffeine/caffeine/3.1.8, Apache-2.0, app maven/mavencentral/com.github.curious-odd-man/rgxgen/1.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.dasniko/testcontainers-keycloak/2.5.0, Apache-2.0, approved, #9175 maven/mavencentral/com.github.docker-java/docker-java-api/3.3.4, Apache-2.0, approved, #10346 +maven/mavencentral/com.github.docker-java/docker-java-api/3.3.6, Apache-2.0, approved, #10346 maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.4, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 +maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.6, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.4, Apache-2.0, approved, #7942 +maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.6, Apache-2.0, approved, #7942 maven/mavencentral/com.github.java-json-tools/btf/1.3, Apache-2.0 OR LGPL-3.0-only, approved, #15201 maven/mavencentral/com.github.java-json-tools/jackson-coreutils/2.0, Apache-2.0 OR LGPL-3.0-or-later, approved, #15186 maven/mavencentral/com.github.java-json-tools/json-patch/1.13, Apache-2.0 OR LGPL-3.0-or-later, approved, CQ23929 @@ -33,9 +36,13 @@ maven/mavencentral/com.google.crypto.tink/tink/1.11.0, Apache-2.0, approved, #10 maven/mavencentral/com.google.errorprone/error_prone_annotations/2.21.1, Apache-2.0, approved, #9834 maven/mavencentral/com.google.protobuf/protobuf-java/3.19.6, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.h2database/h2/2.2.220, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 +maven/mavencentral/com.h2database/h2/2.2.224, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 maven/mavencentral/com.ibm.async/asyncutil/0.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.jayway.jsonpath/json-path/2.9.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/content-type/2.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/lang-tag/1.7, Apache-2.0, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.3, Apache-2.0, approved, #11701 +maven/mavencentral/com.nimbusds/oauth2-oidc-sdk/9.43.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.opencsv/opencsv/5.9, Apache-2.0, approved, clearlydefined maven/mavencentral/com.smartsensesolutions/commons-dao/0.0.5, Apache-2.0, approved, #9176 maven/mavencentral/com.sun.activation/jakarta.activation/1.2.1, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf @@ -70,9 +77,9 @@ maven/mavencentral/io.smallrye.config/smallrye-config-common/2.3.0, Apache-2.0, maven/mavencentral/io.smallrye.config/smallrye-config-core/2.3.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.smallrye.config/smallrye-config/2.3.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.smallrye/jandex/3.1.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.9, Apache-2.0, approved, #5947 -maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.9, Apache-2.0, approved, #5929 -maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.9, Apache-2.0, approved, #5919 +maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.21, Apache-2.0, approved, #5947 +maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.21, Apache-2.0, approved, #5929 +maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.21, Apache-2.0, approved, #5919 maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.3, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/jakarta.annotation/jakarta.annotation-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.ca maven/mavencentral/jakarta.inject/jakarta.inject-api/2.0.1, Apache-2.0, approved, ee4j.cdi @@ -125,9 +132,13 @@ maven/mavencentral/org.hdrhistogram/HdrHistogram/2.2.2, BSD-2-Clause AND CC0-1.0 maven/mavencentral/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final, LGPL-2.1-only, approved, #6962 maven/mavencentral/org.hibernate.orm/hibernate-core/6.5.2.Final, LGPL-2.1-only AND (EPL-2.0 OR BSD-3-Clause) AND LGPL-2.1-or-later AND MIT, approved, #15118 maven/mavencentral/org.hibernate.validator/hibernate-validator/8.0.1.Final, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.8, EPL-2.0, approved, CQ23285 maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.9, EPL-2.0, approved, CQ23285 +maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.8, EPL-2.0, approved, #1068 maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.9, EPL-2.0, approved, #1068 +maven/mavencentral/org.jacoco/org.jacoco.core/0.8.8, EPL-2.0, approved, CQ23283 maven/mavencentral/org.jacoco/org.jacoco.core/0.8.9, EPL-2.0, approved, CQ23283 +maven/mavencentral/org.jacoco/org.jacoco.report/0.8.8, EPL-2.0 AND Apache-2.0, approved, CQ23284 maven/mavencentral/org.jacoco/org.jacoco.report/0.8.9, EPL-2.0 AND Apache-2.0, approved, CQ23284 maven/mavencentral/org.jboss.logging/jboss-logging/3.5.3.Final, Apache-2.0, approved, #9471 maven/mavencentral/org.jboss.resteasy/resteasy-client-api/4.7.7.Final, Apache-2.0, approved, clearlydefined @@ -162,21 +173,25 @@ maven/mavencentral/org.mockito/mockito-inline/5.2.0, MIT, approved, clearlydefin maven/mavencentral/org.mockito/mockito-junit-jupiter/5.11.0, MIT, approved, #13504 maven/mavencentral/org.objenesis/objenesis/3.3, Apache-2.0, approved, clearlydefined maven/mavencentral/org.opentest4j/opentest4j/1.3.0, Apache-2.0, approved, #9713 +maven/mavencentral/org.ow2.asm/asm-analysis/9.2, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/org.ow2.asm/asm-commons/9.2, BSD-3-Clause, approved, clearlydefined maven/mavencentral/org.ow2.asm/asm-commons/9.5, BSD-3-Clause, approved, #7553 +maven/mavencentral/org.ow2.asm/asm-tree/9.2, BSD-3-Clause, approved, clearlydefined maven/mavencentral/org.ow2.asm/asm-tree/9.5, BSD-3-Clause, approved, #7555 +maven/mavencentral/org.ow2.asm/asm/9.2, BSD-3-Clause, approved, CQ23635 maven/mavencentral/org.ow2.asm/asm/9.5, BSD-3-Clause, approved, #7554 maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 maven/mavencentral/org.postgresql/postgresql/42.7.3, BSD-2-Clause AND Apache-2.0, approved, #11681 -maven/mavencentral/org.projectlombok/lombok/1.18.28, MIT, approved, #15192 +maven/mavencentral/org.projectlombok/lombok/1.18.32, MIT, approved, #15192 maven/mavencentral/org.projectlombok/lombok/1.18.34, MIT, approved, #15192 maven/mavencentral/org.reactivestreams/reactive-streams/1.0.4, CC0-1.0, approved, CQ16332 maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined maven/mavencentral/org.skyscreamer/jsonassert/1.5.3, Apache-2.0, approved, clearlydefined maven/mavencentral/org.slf4j/jul-to-slf4j/2.0.13, MIT, approved, #7698 maven/mavencentral/org.slf4j/slf4j-api/2.0.13, MIT, approved, #5915 -maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/2.1.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/2.1.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.1.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/2.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/2.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.5.0, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-actuator/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined @@ -187,6 +202,8 @@ maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/3.3.2, maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-json/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-oauth2-client/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-oauth2-resource-server/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-security/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-test/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/3.3.2, Apache-2.0, approved, clearlydefined @@ -195,6 +212,7 @@ maven/mavencentral/org.springframework.boot/spring-boot-starter-web/3.3.2, Apach maven/mavencentral/org.springframework.boot/spring-boot-starter/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-test/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-testcontainers/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.cloud/spring-cloud-commons/4.1.4, Apache-2.0, approved, #13495 maven/mavencentral/org.springframework.cloud/spring-cloud-context/4.1.4, Apache-2.0, approved, #13494 @@ -206,10 +224,12 @@ maven/mavencentral/org.springframework.data/spring-data-jpa/3.3.2, Apache-2.0, a maven/mavencentral/org.springframework.security/spring-security-config/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-core/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-crypto/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-client/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-oauth2-core/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-oauth2-jose/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-oauth2-resource-server/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-rsa/1.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-test/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-web/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework/spring-aop/6.1.11, Apache-2.0, approved, #15221 maven/mavencentral/org.springframework/spring-aspects/6.1.11, Apache-2.0, approved, #15193 @@ -225,11 +245,16 @@ maven/mavencentral/org.springframework/spring-tx/6.1.11, Apache-2.0, approved, # maven/mavencentral/org.springframework/spring-web/6.1.11, Apache-2.0, approved, #15188 maven/mavencentral/org.springframework/spring-webmvc/6.1.11, Apache-2.0, approved, #15182 maven/mavencentral/org.testcontainers/database-commons/1.19.3, Apache-2.0, approved, #10345 +maven/mavencentral/org.testcontainers/database-commons/1.19.8, Apache-2.0, approved, #10345 maven/mavencentral/org.testcontainers/jdbc/1.19.3, Apache-2.0, approved, #10348 +maven/mavencentral/org.testcontainers/jdbc/1.19.8, Apache-2.0, approved, #10348 maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 +maven/mavencentral/org.testcontainers/junit-jupiter/1.19.8, MIT, approved, #10344 maven/mavencentral/org.testcontainers/postgresql/1.19.3, MIT, approved, #10350 +maven/mavencentral/org.testcontainers/postgresql/1.19.8, MIT, approved, #10350 maven/mavencentral/org.testcontainers/testcontainers/1.19.3, Apache-2.0 AND MIT, approved, #10347 -maven/mavencentral/org.webjars/swagger-ui/4.18.2, Apache-2.0, approved, #15184 +maven/mavencentral/org.testcontainers/testcontainers/1.19.8, MIT, approved, #15203 +maven/mavencentral/org.webjars/swagger-ui/5.13.0, Apache-2.0, approved, #14547 maven/mavencentral/org.wiremock/wiremock-standalone/3.4.2, MIT AND Apache-2.0, approved, #14889 maven/mavencentral/org.xmlunit/xmlunit-core/2.9.1, Apache-2.0, approved, #6272 -maven/mavencentral/org.yaml/snakeyaml/2.0, Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause OR EPL-1.0 OR GPL-2.0-or-later OR LGPL-2.1-or-later), approved, #7275 +maven/mavencentral/org.yaml/snakeyaml/2.2, Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause OR EPL-1.0 OR GPL-2.0-or-later OR LGPL-2.1-or-later), approved, #10232 diff --git a/README.md b/README.md index a4c2920d..244d2c3a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Managed Identity Wallets - The Managed Identity Wallets (MIW) service implements the Self-Sovereign-Identity (SSI) using `did:web`. -# Usage +This is a gradle multi-module project containing two applications: -See [INSTALL.md](INSTALL.md) +1. **miw**: This is a wallet application. Please refer [README.md](miw%2FREADME.md) for more information +2. **revocation-service**: This is verifiable credential revocation service. Please + refer [README.md](revocation-service%2FREADME.md) for more information # Committer Documentation @@ -42,361 +42,14 @@ At this point your repository will behave exactly like upstream when doing a rel # Developer Documentation -To run MIW locally, this section describes the tooling as well as the local development setup. - -There are two possible flows, which can be used for development: - -1. **local**: Run the postgresql and keycloak server inside docker. Start MIW from within your IDE (recommended for - actual development) -2. **docker**: Run everything inside docker (use to test or check behavior inside a docker environment) - -## Tooling - -Following tools the MIW development team used successfully: - -| Area | Tool | Download Link | Comment | -|----------|----------|-------------------------------------------------|--------------------------------------------------------------------------------------------------| -| IDE | IntelliJ | https://www.jetbrains.com/idea/download/ | Use[envfile plugin](https://plugins.jetbrains.com/plugin/7861-envfile) to use the **local** flow | -| Build | Gradle | https://gradle.org/install/ | | -| Runtime | Docker | https://www.docker.com/products/docker-desktop/ | | -| Database | DBeaver | https://dbeaver.io/ | | -| IAM | Keycloak | https://www.keycloak.org/ | | - -## Eclipse Dash Tool - -[Eclipse Dash Homepage](https://projects.eclipse.org/projects/technology.dash) - -The Eclipse Dash tool is used to analyze the dependencies used in the project and ensure all legal requirements are met. -We've added a gradle tasks to download the latest version of Dash locally, resolve all project dependencies and then run -the tool and update the summary in the DEPENDENCIES file. - -To run the license check: - -```bash -./gradlew dashLicenseCheck -``` - -To clean all files created by the dash tasks: - -```bash -./gradlew dashClean -``` - -This command will output all dependencies, save the to file `deps.txt`. Dash will read from the file and update the -summary in the `DEPENDENCIES` file. A committer can open and issue to resolve any problems with the dependencies. - -# Administrator Documentation - -## Manual Keycloak Configuration - -Within the development setup the Keycloak instance is initially prepared with the values -in `./dev-assets/docker-environment/keycloak`. The realm could also be manually added and configured -at http://localhost:8080 via the "Add realm" button. It can be for example named `localkeycloak`. Also add an additional -client, e.g. named `miw_private_client` with *valid redirect url* set to `http://localhost:8080/*`. The roles - -- add_wallets -- view_wallets -- update_wallets -- delete_wallets -- view_wallet -- update_wallet -- manage_app - -Roles can be added under *Clients > miw_private_client > Roles* and then assigned to the client using *Clients > -miw_private_client > Client Scopes* *> Service Account Roles > Client Roles > miw_private_client*. - -The available scopes/roles are: - -1. Role `add_wallets` to create a new wallet -2. Role `view_wallets`: - - to get a list of all wallets - - to retrieve one wallet by its identifier - - to validate a Verifiable Credential - - to validate a Verifiable Presentation - - to get all stored Verifiable Credentials -3. Role `update_wallets` for the following actions: - - to store Verifiable Credential - - to issue a Verifiable Credential - - to issue a Verifiable Presentation -4. Role `update_wallet`: - - to remove a Verifiable Credential - - to store a Verifiable Credential - - to issue a Verifiable Credential - - to issue a Verifiable Presentation -5. Role `view_wallet` requires the BPN of Caller and it can be used: - - to get the Wallet of the related BPN - - to get stored Verifiable Credentials of the related BPN - - to validate any Verifiable Credential - - to validate any Verifiable Presentation -6. Role `manage_app` used to change the log level of the application at runtime. Check Logging in the application - section for more details - -Overview by Endpoint - -| Artefact | CRUD | HTTP Verb/ Request | Endpoint | Roles | Constraints | -|-------------------------------------------|--------|--------------------|---------------------------------------|----------------------------------------------|------------------------------------------------------------| -| **Wallets** | Read | GET | /api/wallets | **view_wallets** | | -| **Wallets** | Create | POST | /api/wallets | **add_wallets** | **1 BPN : 1 WALLET**(PER ONE [1] BPN ONLY ONE [1] WALLET!) | -| **Wallets** | Create | POST | /api/wallets/{identifier}/credentials | **update_wallets**
OR**update_wallet** | | -| **Wallets** | Read | GET | /api/wallets/{identifier} | **view_wallets** OR
**view_wallet** | | -| **Verifiable Presentations - Generation** | Create | POST | /api/presentation | **update_wallets** OR
**update_wallet** | | -| **Verifiable Presentations - Validation** | Create | POST | /api/presentations/validation | **view_wallets** OR
**view_wallet** | | -| **Verifiable Credential - Holder** | Read | GET | /api/credentials | **view_wallets** OR
**view_wallet** | | -| **Verifiable Credential - Holder** | Create | POST | /api/credentials | **update_wallet** OR
**update_wallet** | | -| **Verifiable Credential - Holder** | Delete | DELETE | /api/credentials | **update_wallet** | | -| **Verfiable Credential - Validation** | Create | POST | /api/credentials/validation | **view_wallets** OR
**view_wallet** | | -| **Verfiable Credential - Issuer** | Read | GET | /api/credentials/issuer | **view_wallets** | | -| **Verfiable Credential - Issuer** | Create | POST | /api/credentials/issuer | **update_wallets** | | -| **DIDDocument** | Read | GET | /{bpn}/did.json | N/A | | -| **DIDDocument** | Read | GET | /api/didDocuments/{identifier} | N/A | | - -Additionally, a Token mapper can be created under *Clients* > *ManagedIdentityWallets* > *Mappers* > *create* -with the following configuration (using as an example `BPNL000000001`): - -| Key | Value | -|------------------------------------|-----------------| -| Name | StaticBPN | -| Mapper Type | Hardcoded claim | -| Token Claim Name | BPN | -| Claim value | BPNL000000001 | -| Claim JSON Type | String | -| Add to ID token | OFF | -| Add to access token | ON | -| Add to userinfo | OFF | -| includeInAccessTokenResponse.label | ON | - -If you receive an error message that the client secret is not valid, please go into keycloak admin and within *Clients > -Credentials* recreate the secret. - -## Development Setup - -NOTE: The MIW requires access to the internet in order to validate the JSON-LD schema of DID documents. - -### Prerequisites - -To simplify the dev environment, [Taskfile](https://taskfile.dev) is used as a task executor. You have to install it -first. - -> **IMPORTANT**: Before executing any of th tasks, you have to choose your flow (*local* or *docker*). *local* is -> default. To change that, you need to edit the variable **ENV** in the *Taskfile.yaml*. (see below) - -After that, run `task check-prereqs` to see, if any other required tool is installed or missing. If something is -missing, a link to the install docs is provided. - -Now, you have to adjust the *env* files (located in *dev-assets/env-files*). To do that, copy every file to the same -directory, but without ".dist" at the end. - -Description of the env files: - -- **env.local**: Set up everything to get ready for flow "local". You need to fill in the passwords. -- **env.docker**: Set up everything to get ready for flow "docker". You need to fill in the passwords. - -> **IMPORTANT**: ssi-lib is resolving DID documents over the network. There are two endpoints that rely on this resolution: -> - Verifiable Credentials - Validation -> - Verifiable Presentations - Validation -> -> The following parameters are set in env.local or env.docker file per default: -> ENFORCE_HTTPS_IN_DID_RESOLUTION=false -> MIW_HOST_NAME=localhost -> APPLICATION_PORT=80 -> If you intend to change them, the DID resolving may not work properly anymore! - -> **IMPORTANT**: When you are using macOS and the MIW docker container won't start up (stuck somewhere or doesn't start -> at all), you can enable the docker-desktop feature "Use Rosetta for x86/amd64 emulation on Apple Silicon" in your -> Docker settings (under "features in development"). This should fix the issue. - -Note: *SKIP_GRADLE_TASKS_PARAM* is used to pass parameters to the build process of the MIW jar. Currently, it skips the -tests and code coverage, but speeds up the build time. If you want to activate it, just comment it out -like `SKIP_GRADLE_TASKS_PARAM="" #"-x jacocoTestCoverageVerification -x test"` - -After every execution (either *local* or *docker* flow), run the matching "stop" task ( -e.g.: `task docker:start-app` -> `task docker:stop-app`) - -When you just run `task` without parameters, you will see all tasks available. - -### local - -1. Run `task docker:start-middleware` and wait until it shows "(main) Running the server in development mode. DO NOT use - this configuration in production." in the terminal -2. Run `task app:build` to build the MIW application -3. Run - [ManagedIdentityWalletsApplication.java](src/main/java/org/eclipse/tractusx/managedidentitywallets/ManagedIdentityWalletsApplication.java) - via IDE and use the local.env file to populate environment vars (e.g. EnvFile plugin for IntelliJ) -4. Run `task app:get-token` and copy the token (including "BEARER" prefix) (Mac users have the token already in their - clipboard) -5. Open API doc on http://localhost:8000 (or what port you configured in the *env.local* file) -6. Click on Authorize on swagger UI and on the dialog paste the token into the "value" input -7. Click on "Authorize" and "close" -8. MIW is up and running - -### docker - -1. Run `task docker:start-app` and wait until it shows "Started ManagedIdentityWalletsApplication in ... seconds" -2. Run `task app:get-token` and copy the token (including "BEARER" prefix) (Mac users have the token already in their - clipboard) -3. Open API doc on http://localhost:8000 (or what port you configured in the *env.local* file) -4. Click on Authorize on swagger UI and on the dialog paste the token into the "value" input -5. Click on "Authorize" and "close" -6. MIW is up and running - -### pgAdmin - -This local environment contains [pgAdmin](https://www.pgadmin.org/), which is also started ( -default: http://localhost:8888). -The default login is: - -``` -user: pg@admin.com (you can change it in the env.* files) -password: the one you set for "POSTGRES_PASSWORD" in the env.* files -``` - -#### DB connection password - -When you log in into pgAdmin, the local Postgresql server is already configured. -But you will be asked to enter the DB password on the first time you connect to the DB. -(password: POSTGRES_PASSWORD in the env.* files) - -#### Storage folder - -The storage folder of pgAdmin is mounted to `dev-assets/docker-environment/pgAdmin/storage/`. -For example, You can save DB backups there, so you can access them on your local machine. - -# End Users - -See OpenAPI documentation, which is automatically created from the source and available on each deployment at -the `/docs/api-docs/docs` endpoint (e.g. locally at http://localhost:8087/docs/api-docs/docs). An export of the JSON -document can be also found in [docs/openapi_v001.json](docs/api/openapi_v001.json). - -# Test Coverage - -Jacoco is used to generate the coverage report. The report generation and the coverage verification are automatically -executed after tests. - -The generated HTML report can be found under `jacoco-report/html/` - -To generate the report run the command: - -``` -task app:test-report -``` - -To check the coverage run the command: - -``` -task app:coverage -``` - -Currently, the minimum is 80% coverage. - -# Common issues and solutions during local setup - -## 1. Can not build with test cases - -Test cases are written using the Spring Boot integration test frameworks. These test frameworks start the Spring Boot -test context, which allows us to perform integration testing. In our tests, we utilize the Testcontainers -library (https://java.testcontainers.org/) for managing Docker containers. Specifically, we use Testcontainers to start -PostgreSQL and Keycloak Docker containers locally. - -Before running the tests, please ensure that you have Docker runtime installed and that you have the necessary -permissions to run containers. - -Alternative, you can skip test during the build with `` ./gradlew clean build -x test`` - -## 2. Database migration related issue - -We have implemented database migration using Liquibase (https://www.liquibase.org/). Liquibase allows us to manage -database schema changes effectively. - -In case you encounter any database-related issues, you can resolve them by following these steps: - -1. Delete all tables from the database. -2. Restart the application. -3. Upon restart, the application will recreate the database schema from scratch. - -This process ensures that any issues with the database schema are resolved by recreating it in a fresh state. - -# Environment Variables - -| name | description | default value | -|---------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| APPLICATION_PORT | port number of application | 8080 | -| APPLICATION_ENVIRONMENT | Environment of the application ie. local, dev, int and prod | local | -| DB_HOST | Database host | localhost | -| DB_PORT | Port of database | 5432 | -| DB_NAME | Database name | miw | -| USE_SSL | Whether SSL is enabled in database server | false | -| DB_USER_NAME | Database username | | -| DB_PASSWORD | Database password | | -| DB_POOL_SIZE | Max number of database connection acquired by application | 10 | -| KEYCLOAK_MIW_PUBLIC_CLIENT | Only needed if we want enable login with keyalock in swagger | miw_public | -| MANAGEMENT_PORT | Spring actuator port | 8090 | -| MIW_HOST_NAME | Application host name, this will be used in creation of did ie. did:web:MIW_HOST_NAME:BPN | localhost | -| ENCRYPTION_KEY | encryption key used to encrypt and decrypt private and public key of wallet | | -| AUTHORITY_WALLET_BPN | base wallet BPN number | BPNL000000000000 | -| AUTHORITY_WALLET_NAME | Base wallet name | Catena-X | -| AUTHORITY_WALLET_DID | Base wallet web did | web:did:host:BPNL000000000000 | -| VC_SCHEMA_LINK | Comma separated list of VC schema URL | https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json | -| VC_EXPIRY_DATE | Expiry date of VC (dd-MM-yyyy ie. 01-01-2025 expiry date will be 2024-12-31T18:30:00Z in VC) | 01-01-2025 | -| KEYCLOAK_REALM | Realm name of keycloak | miw_test | -| KEYCLOAK_CLIENT_ID | Keycloak private client id | | -| AUTH_SERVER_URL | Keycloak server url | | -| ENFORCE_HTTPS_IN_DID_RESOLUTION | Enforce https during web did resolution | true | -| APP_LOG_LEVEL | Log level of application | INFO | -| AUTHORITY_SIGNING_SERVICE_TYPE | Base wallet signing type, Currency only LOCAL is supported | Local | -| LOCAL_SIGNING_KEY_STORAGE_TYPE | Key storage type, currently only DB is supported | DB | -| | | | - -# Technical Debts and Known issue - -1. Keys are stored in database in encrypted format, need to store keys in more secure place ie. Vault -2. Policies can be validated dynamically as per request while validating VP and - VC. [Check this for more details](https://docs.walt.id/v/ssikit/concepts/verification-policies) - -# Logging in application - -Log level in application can be set using environment variable ``APP_LOG_LEVEL``. Possible values -are ``OFF, ERROR, WARN, INFO, DEBUG, TRACE`` and default value set to ``INFO`` - -## Change log level at runtime using Spring actuator - -We can use ``/actuator/loggers`` API endpoint of actuator for log related things. This end point can be accessible with -role ``manage_app``. We can add this role to authority wallet client using keycloak as below: - -![manage_app.png](docs%2Fmanage_app.png) - -1. API to get current log settings - ```bash - curl --location 'http://localhost:8090/actuator/loggers' \ - --header 'Authorization: Bearer access_token' - ``` -2. Change log level at runtime - ```bash - curl --location 'http://localhost:8090/actuator/loggers/{java package name}' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Bearer access_token' \ - --data '{"configuredLevel":"INFO"}' - ``` - i.e. - ```bash - curl --location 'http://localhost:8090/actuator/loggers/org.eclipse.tractusx.managedidentitywallets' \ - --header 'Content-Type: application/json' \ - --header 'Authorization: Bearer access_token' \ - --data '{"configuredLevel":"INFO"}' - ``` - -## Reference of external lib - -1. https://www.testcontainers.org/modules/databases/postgres/ -2. https://github.com/dasniko/testcontainers-keycloak -3. https://github.com/smartSenseSolutions/smartsense-java-commons -4. https://github.com/catenax-ng/product-lab-ssi +For end-to-end testing both the application should be running. -## Notice for Docker image +### Common gradle task -See [Docker-hub-notice.md](./Docker-hub-notice.md) +1. Build both the application -## Acknowledgments +``./gradlew clean build`` -We would like to give credit to these projects, which we use in our project. +2. Run tests -[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) +``./gradlew clean test` diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..34271e88 --- /dev/null +++ b/build.gradle @@ -0,0 +1,152 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" + } +} + +plugins { + id "de.undercouch.download" version "5.5.0" +} + +group = "${appGroup}" + +subprojects { + apply { + plugin "java" + plugin "org.springframework.boot" + plugin "io.spring.dependency-management" + plugin 'jacoco-report-aggregation' + + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + } + + repositories { + mavenLocal() + mavenCentral() + maven { + url = uri("https://repo.danubetech.com/repository/maven-public") + } + maven { url 'https://jitpack.io' } + maven { + url = uri("https://repo.eclipse.org/content/repositories/dash-licenses/") + } + } + + dependencies { + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + } + + + tasks.named('test') { + useJUnitPlatform() + + } + bootJar { + enabled = false + } + +} + + +tasks.register('dashDownload', Download) { + description = 'Download the Dash License Tool standalone jar' + group = 'License' + src 'https://repo.eclipse.org/service/local/artifact/maven/redirect?r=dash-licenses&g=org.eclipse.dash&a=org.eclipse.dash.licenses&v=LATEST' + dest rootProject.file('dash.jar') + overwrite false +} + +// This task is primarily used by CIs +tasks.register('dashClean') { + description = "Clean all files used by the 'License' group" + group = 'License' + logger.lifecycle("Removing 'dash.jar'") + rootProject.file('dash.jar').delete() + logger.lifecycle("Removing 'deps.txt'") + file('deps.txt').delete() +} + +tasks.register('dashDependencies') { dashDependencies -> + description = "Output all project dependencies as a flat list and save an intermediate file 'deps.txt'." + group = 'License' + dashDependencies.dependsOn('dashDownload') + doLast { + def deps = [] + project.allprojects.each { project -> + project.configurations.each { conf -> + // resolving 'archives' or 'default' is deprecated + if (conf.canBeResolved && conf.getName() != 'archives' && conf.getName() != 'default') { + deps.addAll( + conf.incoming.resolutionResult.allDependencies + .findAll({ it instanceof ResolvedDependencyResult }) + .collect { ResolvedDependencyResult dep -> + "${dep.selected}" + } + ) + } + } + } + + def finalDeps = [] + for (final def d in deps) { + //skip main module dependencies + if(d.toString() =="project :miw" || d.toString() =="project :revocation-service"){ + println(" - "+d.toString() + " -") + + }else{ + finalDeps.add(d) + } + } + + def uniqueSorted = finalDeps.unique().sort() + uniqueSorted.each { logger.quiet("{}", it) } + file("deps.txt").write(uniqueSorted.join('\n')) + } +} + +tasks.register('dashLicenseCheck', JavaExec) { dashLicenseCheck -> + description = "Run the Dash License Tool and save the summary in the 'DEPENDENCIES' file" + group = 'License' + dashLicenseCheck.dependsOn('dashDownload') + dashLicenseCheck.dependsOn('dashDependencies') + doFirst { + classpath = rootProject.files('dash.jar') + // docs: https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-04 + args('-project', 'automotive.tractusx', '-summary', 'DEPENDENCIES', 'deps.txt') + } + doLast { + logger.lifecycle("Removing 'deps.txt' now.") + file('deps.txt').delete() + } +} diff --git a/docs/api/openapi_v001.json b/docs/api/miw/openapi_v001.json similarity index 99% rename from docs/api/openapi_v001.json rename to docs/api/miw/openapi_v001.json index d2989025..e15bb262 100644 --- a/docs/api/openapi_v001.json +++ b/docs/api/miw/openapi_v001.json @@ -1805,7 +1805,7 @@ ], "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://cofinity-x.github.io/schema-registry/v1.1/DismantlerVC.json", + "https://eclipse=tractusx.github.io/schema-registry/v1.1/DismantlerVC.json", "https://w3id.org/security/suites/jws-2020/v1", "https://w3id.org/vc/status-list/2021/v1" ], diff --git a/gradle.properties b/gradle.properties index 903e4072..6481bbad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,11 @@ jacocoVersion=0.8.9 springBootVersion=3.3.2 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=1.0.0-develop.4 -openApiVersion=2.1.0 +applicationVersion=0.5.0-develop.20 +openApiVersion=2.5.0 +lombokVersion=1.18.32 +gsonVersion=2.10.1 +ssiLibVersion=0.0.19 +wiremockVersion=3.4.2 +commonsDaoVersion=0.0.5 +appGroup=org.eclipse.tractusx.managedidentitywallets diff --git a/Dockerfile b/miw/Dockerfile similarity index 88% rename from Dockerfile rename to miw/Dockerfile index 59c6d84d..589b41d7 100644 --- a/Dockerfile +++ b/miw/Dockerfile @@ -1,5 +1,5 @@ # /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation +# * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional # * information regarding copyright ownership. @@ -27,7 +27,7 @@ RUN apk add curl USER miw -COPY LICENSE NOTICE.md miw/DEPENDENCIES SECURITY.md miw/build/libs/miw-latest.jar /app/ +COPY ../LICENSE NOTICE.md miw/DEPENDENCIES SECURITY.md ./build/libs/miw-latest.jar /app/ WORKDIR /app diff --git a/miw/README.md b/miw/README.md new file mode 100644 index 00000000..25f6e815 --- /dev/null +++ b/miw/README.md @@ -0,0 +1,373 @@ +# Managed Identity Wallets + +The Managed Identity Wallets (MIW) service implements the Self-Sovereign-Identity (SSI) using `did:web`. + +# Usage + +See [INSTALL.md](INSTALL.md) + +# Developer Documentation + +To run MIW locally, this section describes the tooling as well as the local development setup. + +There are two possible flows, which can be used for development: + +1. **local**: Run the postgresql and keycloak server inside docker. Start MIW from within your IDE (recommended for + actual development) +2. **docker**: Run everything inside docker (use to test or check behavior inside a docker environment) + +## Tooling + +Following tools the MIW development team used successfully: + +| Area | Tool | Download Link | Comment | +|----------|----------|-------------------------------------------------|--------------------------------------------------------------------------------------------------| +| IDE | IntelliJ | https://www.jetbrains.com/idea/download/ | Use[envfile plugin](https://plugins.jetbrains.com/plugin/7861-envfile) to use the **local** flow | +| Build | Gradle | https://gradle.org/install/ | | +| Runtime | Docker | https://www.docker.com/products/docker-desktop/ | | +| Database | DBeaver | https://dbeaver.io/ | | +| IAM | Keycloak | https://www.keycloak.org/ | | + +## Eclipse Dash Tool + +[Eclipse Dash Homepage](https://projects.eclipse.org/projects/technology.dash) + +The Eclipse Dash tool is used to analyze the dependencies used in the project and ensure all legal requirements are met. +We've added a gradle tasks to download the latest version of Dash locally, resolve all project dependencies and then run +the tool and update the summary in the DEPENDENCIES file. + +To run the license check: + +```bash +./gradlew dashLicenseCheck +``` + +To clean all files created by the dash tasks: + +```bash +./gradlew dashClean +``` + +This command will output all dependencies, save the to file `deps.txt`. Dash will read from the file and update the +summary in the `DEPENDENCIES` file. A committer can open and issue to resolve any problems with the dependencies. + +# Administrator Documentation + +## Manual Keycloak Configuration + +Within the development setup the Keycloak instance is initially prepared with the values +in `./dev-assets/docker-environment/keycloak`. The realm could also be manually added and configured +at http://localhost:8080 via the "Add realm" button. It can be for example named `localkeycloak`. Also add an additional +client, e.g. named `miw_private_client` with *valid redirect url* set to `http://localhost:8080/*`. The roles + +- add_wallets +- view_wallets +- update_wallets +- delete_wallets +- view_wallet +- update_wallet +- manage_app + +Roles can be added under *Clients > miw_private_client > Roles* and then assigned to the client using *Clients > +miw_private_client > Client Scopes* *> Service Account Roles > Client Roles > miw_private_client*. + +The available scopes/roles are: + +1. Role `add_wallets` to create a new wallet +2. Role `view_wallets`: + - to get a list of all wallets + - to retrieve one wallet by its identifier + - to validate a Verifiable Credential + - to validate a Verifiable Presentation + - to get all stored Verifiable Credentials +3. Role `update_wallets` for the following actions: + - to store Verifiable Credential + - to issue a Verifiable Credential + - to issue a Verifiable Presentation +4. Role `update_wallet`: + - to remove a Verifiable Credential + - to store a Verifiable Credential + - to issue a Verifiable Credential + - to issue a Verifiable Presentation +5. Role `view_wallet` requires the BPN of Caller and it can be used: + - to get the Wallet of the related BPN + - to get stored Verifiable Credentials of the related BPN + - to validate any Verifiable Credential + - to validate any Verifiable Presentation +6. Role `manage_app` used to change the log level of the application at runtime. Check Logging in the application + section for more details + +Overview by Endpoint + +| Artefact | CRUD | HTTP Verb/ Request | Endpoint | Roles | Constraints | +|-------------------------------------------|--------|--------------------|---------------------------------------|----------------------------------------------|------------------------------------------------------------| +| **Wallets** | Read | GET | /api/wallets | **view_wallets** | | +| **Wallets** | Create | POST | /api/wallets | **add_wallets** | **1 BPN : 1 WALLET**(PER ONE [1] BPN ONLY ONE [1] WALLET!) | +| **Wallets** | Create | POST | /api/wallets/{identifier}/credentials | **update_wallets**
OR**update_wallet** | | +| **Wallets** | Read | GET | /api/wallets/{identifier} | **view_wallets** OR
**view_wallet** | | +| **Verifiable Presentations - Generation** | Create | POST | /api/presentation | **update_wallets** OR
**update_wallet** | | +| **Verifiable Presentations - Validation** | Create | POST | /api/presentations/validation | **view_wallets** OR
**view_wallet** | | +| **Verifiable Credential - Holder** | Read | GET | /api/credentials | **view_wallets** OR
**view_wallet** | | +| **Verifiable Credential - Holder** | Create | POST | /api/credentials | **update_wallet** OR
**update_wallet** | | +| **Verifiable Credential - Holder** | Delete | DELETE | /api/credentials | **update_wallet** | | +| **Verfiable Credential - Validation** | Create | POST | /api/credentials/validation | **view_wallets** OR
**view_wallet** | | +| **Verfiable Credential - Issuer** | Read | GET | /api/credentials/issuer | **view_wallets** | | +| **Verfiable Credential - Issuer** | Create | POST | /api/credentials/issuer | **update_wallets** | | +| **Verfiable Credential - Issuer** | Create | POST | /api/credentials/issuer/membership | **update_wallets** | | +| **Verfiable Credential - Issuer** | Create | POST | /api/credentials/issuer/framework | **update_wallets** | | +| **Verfiable Credential - Issuer** | Create | POST | /api/credentials/issuer/distmantler | **update_wallets** | | +| **DIDDocument** | Read | GET | /{bpn}/did.json | N/A | | +| **DIDDocument** | Read | GET | /api/didDocuments/{identifier} | N/A | | + +Additionally, a Token mapper can be created under *Clients* > *ManagedIdentityWallets* > *Mappers* > *create* +with the following configuration (using as an example `BPNL000000001`): + +| Key | Value | +|------------------------------------|-----------------| +| Name | StaticBPN | +| Mapper Type | Hardcoded claim | +| Token Claim Name | BPN | +| Claim value | BPNL000000001 | +| Claim JSON Type | String | +| Add to ID token | OFF | +| Add to access token | ON | +| Add to userinfo | OFF | +| includeInAccessTokenResponse.label | ON | + +If you receive an error message that the client secret is not valid, please go into keycloak admin and within *Clients > +Credentials* recreate the secret. + +## Development Setup + +NOTE: The MIW requires access to the internet in order to validate the JSON-LD schema of DID documents. + +### Prerequisites + +To simplify the dev environment, [Taskfile](https://taskfile.dev) is used as a task executor. You have to install it +first. + +> **IMPORTANT**: Before executing any of th tasks, you have to choose your flow (*local* or *docker*). *local* is +> default. To change that, you need to edit the variable **ENV** in the *Taskfile.yaml*. (see below) + +After that, run `task check-prereqs` to see, if any other required tool is installed or missing. If something is +missing, a link to the install docs is provided. + +Now, you have to adjust the *env* files (located in *dev-assets/env-files*). To do that, copy every file to the same +directory, but without ".dist" at the end. + +Description of the env files: + +- **env.local**: Set up everything to get ready for flow "local". You need to fill in the passwords. +- **env.docker**: Set up everything to get ready for flow "docker". You need to fill in the passwords. + +> **IMPORTANT**: ssi-lib is resolving DID documents over the network. There are two endpoints that rely on this resolution: +> - Verifiable Credentials - Validation +> - Verifiable Presentations - Validation +> +> The following parameters are set in env.local or env.docker file per default: +> ENFORCE_HTTPS_IN_DID_RESOLUTION=false +> MIW_HOST_NAME=localhost +> APPLICATION_PORT=80 +> If you intend to change them, the DID resolving may not work properly anymore! + +> **IMPORTANT**: When you are using macOS and the MIW docker container won't start up (stuck somewhere or doesn't start +> at all), you can enable the docker-desktop feature "Use Rosetta for x86/amd64 emulation on Apple Silicon" in your +> Docker settings (under "features in development"). This should fix the issue. + +Note: *SKIP_GRADLE_TASKS_PARAM* is used to pass parameters to the build process of the MIW jar. Currently, it skips the +tests and code coverage, but speeds up the build time. If you want to activate it, just comment it out +like `SKIP_GRADLE_TASKS_PARAM="" #"-x jacocoTestCoverageVerification -x test"` + +After every execution (either *local* or *docker* flow), run the matching "stop" task ( +e.g.: `task docker:start-app` -> `task docker:stop-app`) + +When you just run `task` without parameters, you will see all tasks available. + +### local + +1. Run `task docker:start-middleware` and wait until it shows "(main) Running the server in development mode. DO NOT use + this configuration in production." in the terminal +2. Run `task app:build` to build the MIW application +3. Run + [ManagedIdentityWalletsApplication.java](src/main/java/org/eclipse/tractusx/managedidentitywallets/ManagedIdentityWalletsApplication.java) + via IDE and use the local.env file to populate environment vars (e.g. EnvFile plugin for IntelliJ) +4. Run `task app:get-token` and copy the token (including "BEARER" prefix) (Mac users have the token already in their + clipboard) +5. Open API doc on http://localhost:8000 (or what port you configured in the *env.local* file) +6. Click on Authorize on swagger UI and on the dialog paste the token into the "value" input +7. Click on "Authorize" and "close" +8. MIW is up and running + +### docker + +1. Run `task docker:start-app` and wait until it shows "Started ManagedIdentityWalletsApplication in ... seconds" +2. Run `task app:get-token` and copy the token (including "BEARER" prefix) (Mac users have the token already in their + clipboard) +3. Open API doc on http://localhost:8000 (or what port you configured in the *env.local* file) +4. Click on Authorize on swagger UI and on the dialog paste the token into the "value" input +5. Click on "Authorize" and "close" +6. MIW is up and running + +### pgAdmin + +This local environment contains [pgAdmin](https://www.pgadmin.org/), which is also started ( +default: http://localhost:8888). +The default login is: + +``` +user: pg@admin.com (you can change it in the env.* files) +password: the one you set for "POSTGRES_PASSWORD" in the env.* files +``` + +#### DB connection password + +When you log in into pgAdmin, the local Postgresql server is already configured. +But you will be asked to enter the DB password on the first time you connect to the DB. +(password: POSTGRES_PASSWORD in the env.* files) + +#### Storage folder + +The storage folder of pgAdmin is mounted to `dev-assets/docker-environment/pgAdmin/storage/`. +For example, You can save DB backups there, so you can access them on your local machine. + +# End Users + +See OpenAPI documentation, which is automatically created from the source and available on each deployment at +the `/docs/api-docs/docs` endpoint (e.g. locally at http://localhost:8087/docs/api-docs/docs). An export of the JSON +document can be also found in [docs/openapi_v001.json](docs/api/openapi_v001.json). + +# Test Coverage + +Jacoco is used to generate the coverage report. The report generation and the coverage verification are automatically +executed after tests. + +The generated HTML report can be found under `jacoco-report/html/` + +To generate the report run the command: + +``` +task app:test-report +``` + +To check the coverage run the command: + +``` +task app:coverage +``` + +Currently, the minimum is 80% coverage. + +# Common issues and solutions during local setup + +## 1. Can not build with test cases + +Test cases are written using the Spring Boot integration test frameworks. These test frameworks start the Spring Boot +test context, which allows us to perform integration testing. In our tests, we utilize the Testcontainers +library (https://java.testcontainers.org/) for managing Docker containers. Specifically, we use Testcontainers to start +PostgreSQL and Keycloak Docker containers locally. + +Before running the tests, please ensure that you have Docker runtime installed and that you have the necessary +permissions to run containers. + +Alternative, you can skip test during the build with `` ./gradlew clean build -x test`` + +## 2. Database migration related issue + +We have implemented database migration using Liquibase (https://www.liquibase.org/). Liquibase allows us to manage +database schema changes effectively. + +In case you encounter any database-related issues, you can resolve them by following these steps: + +1. Delete all tables from the database. +2. Restart the application. +3. Upon restart, the application will recreate the database schema from scratch. + +This process ensures that any issues with the database schema are resolved by recreating it in a fresh state. + +# Environment Variables + +| name | description | default value | +|---------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| APPLICATION_PORT | port number of application | 8080 | +| APPLICATION_ENVIRONMENT | Environment of the application ie. local, dev, int and prod | local | +| DB_HOST | Database host | localhost | +| DB_PORT | Port of database | 5432 | +| DB_NAME | Database name | miw | +| USE_SSL | Whether SSL is enabled in database server | false | +| DB_USER_NAME | Database username | | +| DB_PASSWORD | Database password | | +| DB_POOL_SIZE | Max number of database connection acquired by application | 10 | +| KEYCLOAK_MIW_PUBLIC_CLIENT | Only needed if we want enable login with keyalock in swagger | miw_public | +| MANAGEMENT_PORT | Spring actuator port | 8090 | +| MIW_HOST_NAME | Application host name, this will be used in creation of did ie. did:web:MIW_HOST_NAME:BPN | localhost | +| ENCRYPTION_KEY | encryption key used to encrypt and decrypt private and public key of wallet | | +| AUTHORITY_WALLET_BPN | base wallet BPN number | BPNL000000000000 | +| AUTHORITY_WALLET_NAME | Base wallet name | Catena-X | +| AUTHORITY_WALLET_DID | Base wallet web did | web:did:host:BPNL000000000000 | +| VC_SCHEMA_LINK | Comma separated list of VC schema URL | https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json | +| VC_EXPIRY_DATE | Expiry date of VC (dd-MM-yyyy ie. 01-01-2025 expiry date will be 2024-12-31T18:30:00Z in VC) | 01-01-2025 | +| KEYCLOAK_REALM | Realm name of keycloak | miw_test | +| KEYCLOAK_CLIENT_ID | Keycloak private client id | | +| AUTH_SERVER_URL | Keycloak server url | | +| SUPPORTED_FRAMEWORK_VC_TYPES | Supported framework VC, provide values ie type1=value1,type2=value2 | cx-behavior-twin=Behavior Twin,cx-pcf=PCF,cx-quality=Quality,cx-resiliency=Resiliency,cx-sustainability=Sustainability,cx-traceability=ID_3.0_Trace | +| ENFORCE_HTTPS_IN_DID_RESOLUTION | Enforce https during web did resolution | true | +| CONTRACT_TEMPLATES_URL | Contract templates URL used in summary VC | https://public.catena-x.org/contracts/ | +| APP_LOG_LEVEL | Log level of application | INFO | +| AUTHORITY_SIGNING_SERVICE_TYPE | Base wallet signing type, Currency only LOCAL is supported | Local | +| LOCAL_SIGNING_KEY_STORAGE_TYPE | Key storage type, currently only DB is supported | DB | +| | | | + +# Technical Debts and Known issue + +1. Keys are stored in database in encrypted format, need to store keys in more secure place ie. Vault +2. Policies can be validated dynamically as per request while validating VP and + VC. [Check this for more details](https://docs.walt.id/v/ssikit/concepts/verification-policies) + +# Logging in application + +Log level in application can be set using environment variable ``APP_LOG_LEVEL``. Possible values +are ``OFF, ERROR, WARN, INFO, DEBUG, TRACE`` and default value set to ``INFO`` + +## Change log level at runtime using Spring actuator + +We can use ``/actuator/loggers`` API endpoint of actuator for log related things. This end point can be accessible with +role ``manage_app``. We can add this role to authority wallet client using keycloak as below: + +![manage_app.png](docs%2Fmanage_app.png) + +1. API to get current log settings + ```bash + curl --location 'http://localhost:8090/actuator/loggers' \ + --header 'Authorization: Bearer access_token' + ``` +2. Change log level at runtime + ```bash + curl --location 'http://localhost:8090/actuator/loggers/{java package name}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer access_token' \ + --data '{"configuredLevel":"INFO"}' + ``` + i.e. + ```bash + curl --location 'http://localhost:8090/actuator/loggers/org.eclipse.tractusx.managedidentitywallets' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer access_token' \ + --data '{"configuredLevel":"INFO"}' + ``` + +## Reference of external lib + +1. https://www.testcontainers.org/modules/databases/postgres/ +2. https://github.com/dasniko/testcontainers-keycloak +3. https://github.com/smartSenseSolutions/smartsense-java-commons +4. https://github.com/catenax-ng/product-lab-ssi + +## Notice for Docker image + +See [Docker-hub-notice.md](./Docker-hub-notice.md) + +## Acknowledgments + +We would like to give credit to these projects, which we use in our project. + +[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) diff --git a/miw/build.gradle b/miw/build.gradle index 3ff75754..7e4239c0 100644 --- a/miw/build.gradle +++ b/miw/build.gradle @@ -19,13 +19,8 @@ plugins { id 'java' - id 'org.springframework.boot' version "${springBootVersion}" - id 'io.spring.dependency-management' version "${springDependencyVersion}" id "jacoco" id 'project-report' - - // used to download the 'dash.jar' for license checks - // docs: https://github.com/michel-kraemer/gradle-download-task id "de.undercouch.download" version "5.5.0" } @@ -43,31 +38,8 @@ configurations { compileOnly.extendsFrom(annotaionProcessor) } -repositories { - // delegate is RepositoryHandler - // docs: https://docs.gradle.org/7.6/dsl/org.gradle.api.artifacts.dsl.RepositoryHandler.html - mavenLocal() - mavenCentral() - maven { - url = uri("https://repo.danubetech.com/repository/maven-public") - } - maven { url 'https://jitpack.io' } - maven { - // Used to resolve Dash License Tool - // Dash has a maven plugin, BUT is not resolvable through mavenCentral() - url = uri("https://repo.eclipse.org/content/repositories/dash-licenses/") - } -} - -ext { - -} -// comes from gradle directly dependencies { - // 'implementation', 'testImplementation', 'runtimeOnly', 'compileOnly', 'annotationProcessor' and - // 'testAnnotationProcessor' configuration come from the java-plugin - // docs: https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_plugin_and_dependency_management implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -78,25 +50,13 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation "org.springdoc:springdoc-openapi-starter-common:${openApiVersion}" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${openApiVersion}" - implementation group: 'com.smartsensesolutions', name: 'commons-dao', version: '0.0.5' + implementation "com.smartsensesolutions:commons-dao:${commonsDaoVersion}" implementation 'org.liquibase:liquibase-core' - implementation 'org.eclipse.tractusx.ssi:cx-ssi-lib:0.0.19' - - //Added explicitly to mitigate CVE 2022-1471 - implementation group: 'org.yaml', name: 'snakeyaml', version: '2.0' - - //Added explicitly to mitigate CVE 2023-24998 - implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.5' + implementation "org.eclipse.tractusx.ssi:cx-ssi-lib:${ssiLibVersion}" runtimeOnly 'org.postgresql:postgresql' - compileOnly 'org.projectlombok:lombok' - // custom 'developmentOnly' config - // https://docs.spring.io/spring-boot/docs/2.0.6.RELEASE/reference/html/using-boot-devtools.html#using-boot-devtools - developmentOnly 'org.springframework.boot:spring-boot-devtools' - annotationProcessor 'org.projectlombok:lombok' - testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.wiremock:wiremock-standalone:3.4.2' - testImplementation 'org.projectlombok:lombok:1.18.28' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation "org.testcontainers:testcontainers" testImplementation 'com.h2database:h2:2.2.220' @@ -109,76 +69,6 @@ dependencies { } -// uses the 'download' plugin -// docs: https://plugins.gradle.org/plugin/de.undercouch.download -tasks.register('dashDownload', Download) { - description = 'Download the Dash License Tool standalone jar' - group = 'License' - src 'https://repo.eclipse.org/service/local/artifact/maven/redirect?r=dash-licenses&g=org.eclipse.dash&a=org.eclipse.dash.licenses&v=LATEST' - dest rootProject.file('dash.jar') - // will not replace an existing file. If you know you need a new version - // then manually delete the file yourself, or run `dashClean` - overwrite false -} - -// This task is primarily used by CIs -tasks.register('dashClean') { - description = "Clean all files used by the 'License' group" - group = 'License' - logger.lifecycle("Removing 'dash.jar'") - rootProject.file('dash.jar').delete() - logger.lifecycle("Removing 'deps.txt'") - file('deps.txt').delete() -} - -// Usage: in the root of the project: `./gradlew -q dashDependencies` -// The `-q` option is important if you want to use the output in a pipe. -tasks.register('dashDependencies') { dashDependencies -> - description = "Output all project dependencies as a flat list and save an intermediate file 'deps.txt'." - group = 'License' - dashDependencies.dependsOn('dashDownload') - doLast { - def deps = [] - project.configurations.each { conf -> - // resolving 'archives' or 'default' is deprecated - if (conf.canBeResolved && conf.getName() != 'archives' && conf.getName() != 'default') { - deps.addAll(conf.incoming.resolutionResult.allDependencies - // the 'allDependencies' method return a 'DependencyResult' - // we're only interested in the 'ResolvedDependencyResult' sub-interface - // docs: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/ResolutionResult.html#allDependencies-groovy.lang.Closure- - // docs: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/DependencyResult.html - // docs: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/ResolvedDependencyResult.html - .findAll({ it instanceof ResolvedDependencyResult }) - .collect { ResolvedDependencyResult dep -> - "${dep.selected}" - }) - } - } - - def uniqueSorted = deps.unique().sort() - uniqueSorted.each { logger.quiet("{}", it) } - file("deps.txt").write(uniqueSorted.join('\n')) - } -} - -tasks.register('dashLicenseCheck', JavaExec) { dashLicenseCheck -> - description = "Run the Dash License Tool and save the summary in the 'DEPENDENCIES' file" - group = 'License' - dashLicenseCheck.dependsOn('dashDownload') - dashLicenseCheck.dependsOn('dashDependencies') - doFirst { - classpath = rootProject.files('dash.jar') - // docs: https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-04 - args('-project', 'automotive.tractusx', '-summary', 'DEPENDENCIES', 'deps.txt') - } - doLast { - logger.lifecycle("Removing 'deps.txt' now.") - file('deps.txt').delete() - } -} - -// 'dependencyManagement' comes from the 'io.spring.dependency-management' plugin -// docs: https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/ dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" @@ -186,17 +76,13 @@ dependencyManagement { } } -// 'build' task comes from the 'java' plugin -// docs: https://docs.gradle.org/current/userguide/java_plugin.html build { archivesBaseName = "miw" version = "latest" } -// 'bootJar' comes from the 'org.springframework.boot' plugin -// 'bootJar' is a subclass of the 'jar' task type -// docs: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar bootJar { + enabled = true metaInf { from 'DEPENDENCIES' from '../SECURITY.md' @@ -205,21 +91,15 @@ bootJar { } } -// 'test' comes from the 'java' plugin -// docs: https://docs.gradle.org/current/userguide/java_plugin.html test { useJUnitPlatform() finalizedBy jacocoTestReport } -// standard gradle class -// docs: https://docs.gradle.org/current/dsl/org.gradle.api.reporting.dependencies.HtmlDependencyReportTask.html htmlDependencyReport { projects = project.allprojects } -// 'jacocoTestReport' is provided by the 'jacoco' plugin -// docs: https://docs.gradle.org/current/userguide/jacoco_plugin.html jacocoTestReport { reports { @@ -244,15 +124,11 @@ jacocoTestReport { } } -// 'jacoco' is provided by the 'jacoco' plugin -// docs: https://docs.gradle.org/current/userguide/jacoco_plugin.html#sec:configuring_the_jacoco_plugin jacoco { toolVersion = "${jacocoVersion}" } -// 'jacocoTestCoverageVerification' is provided by the 'jacoco' plugin -// docs: https://docs.gradle.org/current/userguide/jacoco_plugin.html#sec:jacoco_report_violation_rules jacocoTestCoverageVerification { afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { diff --git a/revocation-service/Dockerfile b/revocation-service/Dockerfile new file mode 100644 index 00000000..f2367884 --- /dev/null +++ b/revocation-service/Dockerfile @@ -0,0 +1,15 @@ +FROM eclipse-temurin:latest + +# run as non-root user +#RUN addgroup -g 11111 -S miw && adduser -u 11111 -S -s /bin/false -G miw miw + +# add curl for healthcheck +RUN apt-get update && apt-get install -y curl + +COPY ./revocation-service/build/libs/revocation-service-latest.jar /app/ + +WORKDIR /app + +HEALTHCHECK --start-period=30s CMD curl --fail http://localhost:8090/actuator/health/liveness || exit 1 + +CMD ["java", "-jar", "revocation-service-latest.jar"] diff --git a/revocation-service/README.md b/revocation-service/README.md new file mode 100644 index 00000000..2f27eec6 --- /dev/null +++ b/revocation-service/README.md @@ -0,0 +1,130 @@ +# Bitstring Statuslist Service + +This service is responsible for managing the status of credentials using a Bitstring status list. It supports operations such as creating, revoking, and retrieving credential statuses. + +## Prerequisites + +Before you begin, ensure you have met the following requirements: + +- Java JDK 17 is installed. +- Docker is running if you are using containers for services like Keycloak and Postgres. +- Keycloak service is operational and accessible. +- Postgres database service is running and accessible. +- Environment variables are configured according to the application's requirements. +- MIW is deployed and accessable +- Be sure the right ssi-lib version is installed + +## Environment Configuration + +The application can be configured using environment variables. Below are the available configuration properties with their default values (if any). Ensure these variables are set in your environment before starting the service. + +### Application Configuration + +- **APPLICATION_NAME**: The name of the Spring Boot application. Defaults to "Revocation". +- **APPLICATION_PORT**: The HTTP port for the application. Defaults to 8080. +- **APPLICATION_PROFILE**: The active Spring profile. Defaults to "local". + +### Database Configuration + +- **DATABASE_HOST**: The hostname or IP address of the Postgres database. +- **DATABASE_PORT**: The port number on which the Postgres server is running. Defaults to 5432. +- **DATABASE_NAME**: The name of the database to connect to. +- **DATABASE_USERNAME**: The username for accessing the database. +- **DATABASE_USE_SSL_COMMUNICATION**: Whether to use SSL for database communication. Defaults to false. +- **DATABASE_PASSWORD**: The password for accessing the database. +- **DATABASE_CONNECTION_POOL_SIZE**: The size of the database connection pool. Defaults to 10. + +### Swagger Configuration + +- **ENABLE_SWAGGER_UI**: Flag to enable or disable Swagger UI. Defaults to false. +- **ENABLE_API_DOC**: Flag to enable or disable API documentation. Defaults to false. + +### Logging Configuration + +- **APPLICATION_LOG_LEVEL**: The application-wide log level. Defaults to "DEBUG". + +### Security Configuration + +The application integrates with Keycloak for OAuth2 authentication and authorization: + +- **SERVICE_SECURITY_ENABLED**: Flag to enable or disable Servive Security integration for Disabling Swagger and other Endpoints. Defaults to true, false only for test purposes recommended. + +The application integrates with Keycloak for OAuth2 authentication and authorization: + +- **KEYCLOAK_ENABLED**: Flag to enable or disable Keycloak integration. Defaults to true. +- **KEYCLOAK_REALM**: The Keycloak realm to connect to. +- **KEYCLOAK_CLIENT_ID**: The Keycloak client ID for the application. +- **KEYCLOAK_PUBLIC_CLIENT_ID**: The Keycloak public client ID, used for Swagger UI authentication. +- **AUTH_SERVER_URL**: The URL for the Keycloak authentication server. + +Keycloak URLs for token and authentication management: + +- **KEYCLOAK_AUTH_URL**: Constructed from `AUTH_SERVER_URL`, `KEYCLOAK_REALM`. +- **KEYCLOAK_TOKEN_URL**: Constructed from `AUTH_SERVER_URL`, `KEYCLOAK_REALM`. +- **KEYCLOAK_REFRESH_TOKEN_URL**: Same as `KEYCLOAK_TOKEN_URL`. +- **KEYCLOAK_USERNAME**: The username for accessing the Keycloak management APIs. +- **KEYCLOAK_PASSWORD**: The password for accessing the Keycloak management APIs. + +### External Service URLs + +- **MIW_URL**: The URL for the Middleware (MIW) used for signing status list credentials. +- **DOMAIN_URL**: The base URL for your domain, which may be used for service-to-service communication or callbacks. + +## Spring Boot Configuration + +The `server`, `spring`, `springdoc`, `management`, and `logging` sections of the YAML are Spring Boot-specific configurations. They configure the application's behavior, data source, OpenAPI documentation, and logging levels, among other things. + +Be sure to replace placeholder values in the environment variables with actual data according to your application's specific requirements. + +## Middleware (MIW) Setup + +Ensure that the middleware (MIW) is running, as it is used to sign the status list credentials. + +An Overview how to start the middleware can be found under the Readme.md in here:[README.md](..%2Fmiw%2FREADME.md) + +## Starting Services + +To start the Bitstring Statuslist Service, follow these steps: + +1. **Start Keycloak and Postgres:** + + Ensure that both Keycloak and Postgres services are running. For development purposes the Keycloak and + Postgres from the MIW Dev Setup can be used if not + already running with the MIW Task deployment. + + [dev-assets](..%2Fdev-assets) + + For starting it, follow the Guidelines in the MIW-Repo Readme.md -> Development Setup -> local + with `task docker:start-middleware` or manually + call start the docker compose file. + +2. **Start the Service:** + +Execute the following command from the root of the project to start the service: +./gradlew bootRun + +## Access Swagger UI + +After starting the service, access the Swagger UI to test API endpoints by navigating to the following URL: +http://localhost:8080/ui/swagger-ui/ + +Replace `localhost` and `8080` with your service's host and port if they are different. + +## Build and Run Using Gradle + +- To build the project, run the following command: + +``` +cd revpcation-service +./../gradlew clean build +``` + +- To run the tests: +``` +cd revpcation-service +./../gradlew clean test +``` + +## Additional Information + +For more information on how to configure and use the service, refer to the provided documentation or contact the development team. diff --git a/revocation-service/build.gradle b/revocation-service/build.gradle new file mode 100644 index 00000000..a5f0ef39 --- /dev/null +++ b/revocation-service/build.gradle @@ -0,0 +1,134 @@ +/******************************************************************************** + * Copyright (c) 2021, 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + id 'java' + id 'jacoco' + id 'project-report' +} + +group = "${groupName}" +version = "${applicationVersion}" +sourceCompatibility = JavaVersion.VERSION_17 + +jar { + enabled = false +} + +java { + sourceCompatibility = '17' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation "org.springdoc:springdoc-openapi-starter-common:${openApiVersion}" + implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${openApiVersion}" + implementation "com.google.code.gson:gson:${gsonVersion}" + implementation 'org.liquibase:liquibase-core' + implementation "org.eclipse.tractusx.ssi:cx-ssi-lib:${ssiLibVersion}" + compileOnly "org.projectlombok:lombok:${lombokVersion}" + developmentOnly 'org.springframework.boot:spring-boot-devtools' + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testImplementation "org.wiremock:wiremock-standalone:${wiremockVersion}" + testImplementation "org.projectlombok:lombok:${lombokVersion}" + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:postgresql' + testImplementation 'com.h2database:h2' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +build { + archivesBaseName = "revocation-service" + version = "latest" + +} + +tasks.named('test') { + useJUnitPlatform() +} + +tasks.test { + finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run + testLogging { + events("passed", "skipped", "failed") + } +} +tasks.jacocoTestReport { + dependsOn(tasks.test) + reports { + xml.required = true + csv.required = false + html.required = true + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.class", + "org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.class" + ]) + })) + } +} + +htmlDependencyReport { + projects = project.allprojects +} + + + + +jacocoTestCoverageVerification { + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.class", + "org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.class" + ]) + })) + } + violationRules { + rule { + limit { + minimum = 0.8 + } + } + } +} + +check.dependsOn jacocoTestCoverageVerification diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/VerifiableCredentialsRevocationServiceApplication.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/VerifiableCredentialsRevocationServiceApplication.java new file mode 100644 index 00000000..d25d7868 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/VerifiableCredentialsRevocationServiceApplication.java @@ -0,0 +1,35 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +@SpringBootApplication +@ConfigurationPropertiesScan +public class VerifiableCredentialsRevocationServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(VerifiableCredentialsRevocationServiceApplication.class, args); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java new file mode 100644 index 00000000..168e094a --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java @@ -0,0 +1,199 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.apidocs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class RevocationApiControllerApiDocs { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Verifiable credential revoked successfully.", + content = @Content()), + @ApiResponse( + responseCode = "401", + description = "UnauthorizedException: invalid token", + content = @Content()), + @ApiResponse( + responseCode = "403", + description = "ForbiddenException: invalid caller", + content = @Content()), + @ApiResponse( + responseCode = "409", + description = "ConflictException: Revocation service error", + content = + @Content( + examples = + @ExampleObject( + value = + "{\n" + + " \"type\": \"about:blank\",\n" + + " \"title\": \"Revocation service error\",\n" + + " \"status\": 409,\n" + + " \"detail\": \"Credential already revoked\",\n" + + " \"type\": \"BitstringStatusListEntry\",\n" + + " \"instance\": \"/api/v1/revocations/revoke\",\n" + + " \"timestamp\": 1707133388128\n" + + "}"), + mediaType = "application/json")), + @ApiResponse( + responseCode = "500", + description = "RevocationServiceException: Internal Server Error", + content = @Content()) + }) + @RequestBody( + content = { + @Content( + examples = + @ExampleObject( + value = + "{\n" + + " \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0\",\n" + + " \"statusPurpose\": \"revocation\",\n" + + " \"statusListIndex\": \"0\",\n" + + " \"statusListCredential\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\",\n" + + " \"type\": \"BitstringStatusListEntry\"\n" + + "}"), + mediaType = "application/json") + }) + @Operation( + summary = "Revoke a VerifiableCredential", + description = "Revoke a VerifiableCredential using the provided Credential Status") + public @interface revokeCredentialDocs { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Status list credential created/updated successfully.", + content = { + @Content( + examples = + @ExampleObject( + value = + "{\n" + + " \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0\",\n" + + " \"statusPurpose\": \"revocation\",\n" + + " \"statusListIndex\": \"0\",\n" + + " \"statusListCredential\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\",\n" + + " \"type\": \"BitstringStatusListEntry\"\n" + + "}"), + mediaType = "application/json") + }), + @ApiResponse( + responseCode = "401", + description = "UnauthorizedException: invalid token", + content = @Content()), + @ApiResponse( + responseCode = "403", + description = "ForbiddenException: invalid caller", + content = @Content()), + @ApiResponse( + responseCode = "500", + description = "RevocationServiceException: Internal Server Error", + content = @Content()) + }) + @RequestBody( + content = { + @Content( + examples = + @ExampleObject( + value = + "{\n" + + " \"purpose\": \"revocation\",\n" + + " \"issuerId\": \"did:web:localhost:BPNL000000000000\"\n" + + "}"), + mediaType = "application/json") + }) + @Operation( + summary = "Create or Update a Status List Credential", + description = "Create the status list credential if it does not exist, else update it.") + public @interface StatusEntryApiDocs { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Get Status list credential ", + content = { + @Content( + examples = + @ExampleObject( + value = + "{\"@context\": [\"https://www.w3.org/2018/credentials/v1\", \"https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json\", \"https://w3id.org/security/suites/jws-2020/v1\"], \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\", \"type\": [\"VerifiableCredential\", \"BitstringStatusListCredential\"], \"issuer\": \"did:web:localhost:BPNL000000000000\", \"issuanceDate\": \"2024-02-05T09:39:58Z\", \"credentialSubject\": [{\"statusPurpose\": \"revocation\", \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\", \"type\": \"BitstringStatusList\", \"encodedList\": \"H4sIAAAAAAAA/wMAAAAAAAAAAAA=\"}], \"proof\": {\"proofPurpose\": \"assertionMethod\", \"type\": \"JsonWebSignature2020\", \"verificationMethod\": \"did:web:localhost:BPNL000000000000#ed463e4c-b900-481a-b5d0-9ae439c434ae\", \"created\": \"2024-02-05T09:39:58Z\", \"jws\": \"eyJhbGciOiJFZERTQSJ9..swX1PLJkSlxB6JMmY4a2uUzR-uszlyLrVdNppoYSx4PTV1LzQrDb0afzp_dvTNUWEYDI57a8iPh78BDjqMjSDQ\"}}"), + mediaType = "application/json") + }), + @ApiResponse( + responseCode = "404", + description = "Status list credential not found", + content = @Content()), + @ApiResponse( + responseCode = "500", + description = "RevocationServiceException: Internal Server Error", + content = @Content()) + }) + @Operation( + summary = "Get status list credential", + description = + "Get status list credential using the provided issuer BPN and status purpose and status list index") + public @interface GetStatusListCredentialDocs { + } + + @Parameter(description = "Issuer BPN", example = "BPNL000000000000") + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + public @interface IssuerBPNPathParamDoc { + } + + @Parameter(description = "Status Purpose ( Revocation or Suspension)", example = "revocation") + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + public @interface StatusPathParamDoc { + } + + @Parameter(description = "status list index", example = "1") + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + public @interface IndexPathParamDoc { + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ApplicationConfig.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ApplicationConfig.java new file mode 100644 index 00000000..6664b30e --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ApplicationConfig.java @@ -0,0 +1,51 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.text.StringEscapeUtils; +import org.springdoc.core.properties.SwaggerUiConfigProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@ConditionalOnProperty(name = "springdoc.swagger-ui.enabled", havingValue = "true") +@Slf4j +public class ApplicationConfig implements WebMvcConfigurer { + + private final SwaggerUiConfigProperties properties; + + @Autowired + public ApplicationConfig(SwaggerUiConfigProperties properties) { + this.properties = properties; + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + String redirectUri = properties.getPath(); + log.info("Set landing page to path {}", StringEscapeUtils.escapeJava(redirectUri)); + registry.addRedirectViewController("/", redirectUri); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java new file mode 100644 index 00000000..e237b86a --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java @@ -0,0 +1,67 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.CredentialAlreadyRevokedException; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.ForbiddenException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +@Slf4j +public class ExceptionHandling { + + public static final String TIMESTAMP_KEY = "timestamp"; + + @ExceptionHandler(CredentialAlreadyRevokedException.class) + ProblemDetail handleCredentialAlreadyRevokedException(CredentialAlreadyRevokedException e) { + String errorMsg = e.getMessage(); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, errorMsg); + problemDetail.setTitle("Revocation service error"); + problemDetail.setProperty(TIMESTAMP_KEY, System.currentTimeMillis()); + return problemDetail; + } + + + @ExceptionHandler(ForbiddenException.class) + ProblemDetail handleForbiddenException(ForbiddenException e) { + String errorMsg = ExceptionUtils.getMessage(e); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, errorMsg); + problemDetail.setTitle(errorMsg); + problemDetail.setProperty(TIMESTAMP_KEY, System.currentTimeMillis()); + return problemDetail; + } + + @ExceptionHandler(IllegalArgumentException.class) + ProblemDetail handleIllegalArgumentException(IllegalArgumentException e) { + String errorMsg = ExceptionUtils.getMessage(e); + ProblemDetail problemDetail = + ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, errorMsg); + problemDetail.setTitle(errorMsg); + problemDetail.setProperty(TIMESTAMP_KEY, System.currentTimeMillis()); + return problemDetail; + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettings.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettings.java new file mode 100644 index 00000000..67a71e9f --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettings.java @@ -0,0 +1,40 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.net.URI; +import java.util.List; + +@ConfigurationProperties(prefix = "revocation.miw") +public record MIWSettings(List vcContexts) { + + public MIWSettings { + if (vcContexts == null) { + throw new NullPointerException("vcContexts cannot be null"); + } + if (vcContexts.isEmpty()) { + throw new IllegalArgumentException("vcContexts cannot be empty"); + } + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfig.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfig.java new file mode 100644 index 00000000..0f09edc7 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfig.java @@ -0,0 +1,106 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.AllArgsConstructor; +import org.eclipse.tractusx.managedidentitywallets.revocation.config.security.SecurityConfigProperties; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; + +import java.util.Collections; + +/** + * OpenApiConfig is used for managing the swagger with basic security setup if security is enabled. + */ +@Configuration +@AllArgsConstructor +public class OpenApiConfig { + + private final SecurityConfigProperties properties; + + /** + * Open api open api. + * + * @return the open api + */ + @Bean + public OpenAPI openAPI() { + Info info = new Info(); + info.setTitle("Reovcation Service API"); + info.setDescription("Revocation Service API"); + info.setVersion("0.0.1"); + + Contact contact = new Contact(); + contact.name("eclipse-tractusx"); + contact.email("tractusx-dev@eclipse.org"); + contact.url("https://projects.eclipse.org/projects/automotive.tractusx"); + info.contact(contact); + + OpenAPI openAPI = new OpenAPI(); + if (Boolean.TRUE.equals(properties.enabled())) { + openAPI = enableSecurity(openAPI); + } + return openAPI.info(info); + } + + /** + * Open api definition grouped open api. + * + * @return the grouped open api + */ + @Bean + public GroupedOpenApi openApiDefinition() { + return GroupedOpenApi.builder().group("docs").pathsToMatch("/**").displayName("Docs").build(); + } + + private OpenAPI enableSecurity(OpenAPI openAPI) { + Components components = new Components(); + + // Auth using access_token + String accessTokenAuth = "Authenticate using access_token"; + components.addSecuritySchemes( + accessTokenAuth, + new SecurityScheme() + .name(accessTokenAuth) + .description( + """ + **Bearer (apiKey)** + JWT Authorization header using the Bearer scheme. + Enter **Bearer** [space] and then your token in the text input below. + Example: Bearer 12345abcdef""") + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(HttpHeaders.AUTHORIZATION)); + return openAPI + .components(components) + .addSecurityItem( + new SecurityRequirement().addList(accessTokenAuth, Collections.emptyList())); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverter.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverter.java new file mode 100644 index 00000000..92bca9d8 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverter.java @@ -0,0 +1,78 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config.security; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * The type Custom authentication converter. + */ +public class CustomAuthenticationConverter implements Converter { + + private final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter; + private final String resourceId; + + /** + * Instantiates a new Custom authentication converter. + * + * @param resourceId the resource id + */ + public CustomAuthenticationConverter(String resourceId) { + this.resourceId = resourceId; + grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + } + + @Override + public AbstractAuthenticationToken convert(Jwt source) { + Collection authorities = grantedAuthoritiesConverter.convert(source); + Optional.of(authorities).ifPresent(a -> a.addAll(extractResourceRoles(source, resourceId))); + return new JwtAuthenticationToken(source, authorities); + } + + private Collection extractResourceRoles(Jwt jwt, String resourceId) { + Map resourceAccess = jwt.getClaim("resource_access"); + Map resource = (Map) resourceAccess.get(resourceId); + if (Objects.isNull(resource)) { + return Set.of(); + } + Collection resourceRoles = (Collection) resource.get("roles"); + if (Objects.isNull(resourceRoles)) { + return Set.of(); + } + return resourceRoles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toSet()); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java new file mode 100644 index 00000000..1424ba96 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java @@ -0,0 +1,119 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config.security; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.ApplicationRole; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAnyRole; + +@Slf4j +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +@Configuration +@AllArgsConstructor +public class SecurityConfig { + + private final SecurityConfigProperties securityConfigProperties; + + /** + * Filter chain security filter chain. + * + * @param http the http + * @return the security filter chain + * @throws Exception the exception + */ + @Bean + @ConditionalOnProperty( + value = "service.security.enabled", + havingValue = "true", + matchIfMissing = true) + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors(Customizer.withDefaults()) + .csrf(AbstractHttpConfigurer::disable) + .headers( + httpSecurityHeadersConfigurer -> + httpSecurityHeadersConfigurer + .xssProtection(Customizer.withDefaults()) + .contentSecurityPolicy( + contentSecurityPolicyConfig -> + contentSecurityPolicyConfig.policyDirectives("script-src 'self'"))) + .sessionManagement( + sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests( + authorizeHttpRequests -> + authorizeHttpRequests + .requestMatchers("/") + .permitAll() // forwards to swagger + .requestMatchers("/docs/api-docs/**") + .permitAll() + .requestMatchers("/ui/swagger-ui/**") + .permitAll() + .requestMatchers("/error") + .permitAll() + .requestMatchers(new AntPathRequestMatcher("/actuator/health/**")) + .permitAll() + .requestMatchers(new AntPathRequestMatcher("/actuator/loggers/**")) + .hasRole(ApplicationRole.ROLE_MANAGE_APP) + .requestMatchers( + HttpMethod.GET, RevocationApiEndpoints.REVOCATION_API + "/credentials/**") + .permitAll() + .requestMatchers(RevocationApiEndpoints.REVOCATION_API + "/**") + .access(hasAnyRole(ApplicationRole.ROLE_UPDATE_WALLET))) + .oauth2ResourceServer( + resourceServer -> + resourceServer.jwt( + jwt -> + jwt.jwtAuthenticationConverter( + new CustomAuthenticationConverter( + securityConfigProperties.clientId())))); + return http.build(); + } + + /** + * Security customizer web security customizer. + * + * @return the web security customizer + */ + @Bean + @ConditionalOnProperty(value = "service.security.enabled", havingValue = "false") + public WebSecurityCustomizer securityCustomizer() { + log.warn("Disable security : This is not recommended to use in production environments."); + return web -> web.ignoring().requestMatchers(new AntPathRequestMatcher("**")); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.java new file mode 100644 index 00000000..0c981ed1 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.java @@ -0,0 +1,37 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * The type Security config properties. + */ +@ConfigurationProperties("revocation.security.keycloak") +public record SecurityConfigProperties( + Boolean enabled, + String clientId, + String publicClientId, + String authUrl, + String tokenUrl, + String refreshTokenUrl) { +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java new file mode 100644 index 00000000..11722b79 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java @@ -0,0 +1,33 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.constant; + +public class ApplicationRole { + + public static final String ROLE_MANAGE_APP = "manage_app"; + + public static final String ROLE_UPDATE_WALLET = "update_wallet"; + + private ApplicationRole() { + // static + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java new file mode 100644 index 00000000..00f90ef8 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java @@ -0,0 +1,27 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.constant; + +public enum PurposeType { + REVOCATION, + SUSPENSION, +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java new file mode 100644 index 00000000..b91f949e --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java @@ -0,0 +1,37 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.constant; + +public class RevocationApiEndpoints { + + public static final String REVOCATION_API = "/api/v1/revocations"; + public static final String CREDENTIALS = "/api/credentials"; + public static final String REVOKE = "/revoke"; + public static final String STATUS_ENTRY = "/status-entry"; + public static final String CREDENTIALS_BY_ISSUER = "/credentials"; + public static final String CREDENTIALS_STATUS_INDEX = + CREDENTIALS_BY_ISSUER + "/{issuerBPN}/{status}/{index}"; + + private RevocationApiEndpoints() { + // static + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java new file mode 100644 index 00000000..0c982b91 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java @@ -0,0 +1,52 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.controllers; + +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.StringPool; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.Validate; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.security.Principal; +import java.util.Map; +import java.util.TreeMap; + +public class BaseController { + + /** + * Gets bpn from token. + * + * @param principal the principal + * @return the bpn from token + */ + public String getBPNFromToken(Principal principal) { + Object principal1 = ((JwtAuthenticationToken) principal).getPrincipal(); + Jwt jwt = (Jwt) principal1; + // this will misbehave if we have more then one claims with different case + // ie. BPN=123456 and bpn=789456 + Map claims = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + claims.putAll(jwt.getClaims()); + Validate.isFalse(claims.containsKey(StringPool.BPN)) + .launch(new SecurityException("Invalid token, BPN not found")); + return claims.get(StringPool.BPN).toString(); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java new file mode 100644 index 00000000..f4b0d82f --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java @@ -0,0 +1,136 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.controllers; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.revocation.apidocs.RevocationApiControllerApiDocs; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.RevocationServiceException; +import org.eclipse.tractusx.managedidentitywallets.revocation.services.RevocationService; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.Validate; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +/** + * The RevocationApiController class is a REST controller that handles revocation-related API + * endpoints. + */ +@RestController +@RequestMapping(RevocationApiEndpoints.REVOCATION_API) +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Revocation Service", description = "Revocation Service API") +public class RevocationApiController extends BaseController { + + private final RevocationService revocationService; + + /** + * The above function is a Java POST endpoint that creates a status list for a credential using + * the provided DTO. + * + * @param dto The parameter "dto" is of type "StatusEntryDto" and is annotated with + * "@RequestBody". It is used to receive the request body data from the client. The "@Valid" + * annotation is used to perform validation on the "dto" object based on the validation + * constraints defined in the "StatusEntry + * @param token The authentication token + * @return The method is returning a CredentialStatusDto object. + */ + @RevocationApiControllerApiDocs.StatusEntryApiDocs + @PostMapping( + value = RevocationApiEndpoints.STATUS_ENTRY, + produces = MediaType.APPLICATION_JSON_VALUE) + public CredentialStatusDto createStatusListVC( + @Valid @RequestBody StatusEntryDto dto, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token, + Principal principal) { + Validate.isFalse( + getBPNFromToken(principal).equals(revocationService.extractBpnFromDid(dto.issuerId()))) + .launch(new ForbiddenException("invalid caller")); + return revocationService.createStatusList(dto, token); + } + + /** + * The above function is a Java POST endpoint that revokes a credential and returns an HTTP status + * code. + * + * @param dto The `dto` parameter is of type `CredentialStatusDto` and is annotated with + * `@RequestBody`. This means that it is expected to be the request body of the HTTP POST + * request. The `@Valid` annotation indicates that the `dto` object should be validated before + * being processed further. + * @param token The authentication token + * @return The method is returning a ResponseEntity object with a HttpStatus of OK. + */ + @RevocationApiControllerApiDocs.revokeCredentialDocs + @PostMapping(RevocationApiEndpoints.REVOKE) + public ResponseEntity revokeCredential( + @Valid @RequestBody CredentialStatusDto dto, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token, + Principal principal) + throws RevocationServiceException { + Validate.isFalse( + getBPNFromToken(principal).equals(revocationService.extractBpnFromURL(dto.id()))) + .launch(new ForbiddenException("Invalid caller")); + revocationService.revoke(dto, token); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * The function `getCredentialsByIssuerId` retrieves a list of credentials by their issuer ID. + * + * @param issuerId The `issuerId` parameter is a string that represents the identifier of the + * issuer. + * @return The method is returning a ResponseEntity object that wraps a VerifiableCredential + * object. + */ + @RevocationApiControllerApiDocs.GetStatusListCredentialDocs + @GetMapping( + path = RevocationApiEndpoints.CREDENTIALS_STATUS_INDEX, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getStatusListCredential( + @RevocationApiControllerApiDocs.IssuerBPNPathParamDoc @PathVariable(name = "issuerBPN") String issuerBPN, + @RevocationApiControllerApiDocs.StatusPathParamDoc @PathVariable(name = "status") String status, + @RevocationApiControllerApiDocs.IndexPathParamDoc @PathVariable(name = "index") String index) { + log.debug("received get list for {}", issuerBPN); + return ResponseEntity.ofNullable( + revocationService.getStatusListCredential( + issuerBPN.toUpperCase(), status.toLowerCase(), index)); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPN.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPN.java new file mode 100644 index 00000000..9a53b007 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPN.java @@ -0,0 +1,62 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.domain; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BPN { + + private static final String PATTERN = "^(BPN)([LSA])[0-9A-Z]{12}"; + + private final String value; + + public BPN(final String bpn) { + + Pattern pattern = Pattern.compile(PATTERN); + Matcher matcher = pattern.matcher(bpn); + if (!matcher.matches()) { + throw new IllegalArgumentException("BPN %s is not valid".formatted(bpn)); + } + + this.value = bpn; + } + + public String value() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final BPN bpn = (BPN) o; + + return value.equals(bpn.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java new file mode 100644 index 00000000..cf3ce0b1 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java @@ -0,0 +1,48 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; + +public record CredentialStatusDto( + @NotBlank @NotNull @JsonProperty("id") String id, + @NotBlank @NotNull @JsonProperty("statusPurpose") String statusPurpose, + @NotBlank @NotNull @JsonProperty("statusListIndex") String statusListIndex, + @NotBlank @NotNull @JsonProperty("statusListCredential") String statusListCredential, + @NotBlank @NotNull @JsonProperty("type") String type) { + public CredentialStatusDto { + if (Integer.parseInt(statusListIndex) < 0 + || Integer.parseInt(statusListIndex) > BitSetManager.BITSET_SIZE - 1) { + throw new IllegalArgumentException("statusListIndex is out of range"); + } + if (!statusPurpose.equalsIgnoreCase(PurposeType.REVOCATION.toString())) { + throw new IllegalArgumentException("invalid statusPurpose"); + } + if (!type.equals(StatusListCredentialSubject.TYPE_ENTRY)) { + throw new IllegalArgumentException("invalid type"); + } + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java new file mode 100644 index 00000000..9a77d39e --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java @@ -0,0 +1,41 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; + +@Valid +public record StatusEntryDto( + // #TODO should be an Identifier (add validation) + @NotNull @NotBlank @JsonProperty("purpose") String purpose, + @NotNull @NotBlank @JsonProperty("issuerId") String issuerId) { + + public StatusEntryDto { + if (!purpose.equalsIgnoreCase(PurposeType.REVOCATION.toString())) { + throw new IllegalArgumentException("purpose should only be revocation at this time"); + } + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java new file mode 100644 index 00000000..0482d48b --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java @@ -0,0 +1,42 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder() +public class StatusListCredentialSubject { + public static final String TYPE_ENTRY = "BitstringStatusListEntry"; + public static final String TYPE_CREDENTIAL = "BitstringStatusListCredential"; + public static final String TYPE_LIST = "BitstringStatusList"; + + public static final String SUBJECT_ID = "id"; + public static final String SUBJECT_TYPE = "type"; + public static final String SUBJECT_STATUS_PURPOSE = "statusPurpose"; + public static final String SUBJECT_ENCODED_LIST = "encodedList"; + private final String type; + private String id; + private String statusPurpose; + private String encodedList; +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponse.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponse.java new file mode 100644 index 00000000..9dc3def9 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponse.java @@ -0,0 +1,34 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TokenResponse { + + @JsonAlias("access_token") + private String accessToken; +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerException.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerException.java new file mode 100644 index 00000000..c20bbc12 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerException.java @@ -0,0 +1,45 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.exception; + +public class BitSetManagerException extends RuntimeException { + + public BitSetManagerException() { + } + + public BitSetManagerException(String message) { + super(message); + } + + public BitSetManagerException(String message, Throwable cause) { + super(message, cause); + } + + public BitSetManagerException(Throwable cause) { + super(cause); + } + + public BitSetManagerException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/CredentialAlreadyRevokedException.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/CredentialAlreadyRevokedException.java new file mode 100644 index 00000000..7d8ac72c --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/CredentialAlreadyRevokedException.java @@ -0,0 +1,29 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.exception; + +public class CredentialAlreadyRevokedException extends BitSetManagerException { + + public CredentialAlreadyRevokedException(String message) { + super(message); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java new file mode 100644 index 00000000..a5420c34 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java @@ -0,0 +1,59 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.exception; + +public class ForbiddenException extends RuntimeException { + + /** + * Instantiates a new Forbidden exception. + */ + public ForbiddenException() { + } + + /** + * Instantiates a new Forbidden exception. + * + * @param message the message + */ + public ForbiddenException(String message) { + super(message); + } + + /** + * Instantiates a new Forbidden exception. + * + * @param message the message + * @param cause the cause + */ + public ForbiddenException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Instantiates a new Forbidden exception. + * + * @param cause the cause + */ + public ForbiddenException(Throwable cause) { + super(cause); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceException.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceException.java new file mode 100644 index 00000000..3e0672fd --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceException.java @@ -0,0 +1,62 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.exception; + +/** + * Custom exception class to encapsulate service-related exceptions. + */ +public class RevocationServiceException extends Exception { + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized. + * + * @param message the detail message. The detail message is saved for later retrieval by the + * {@link #getMessage()} method. + */ + public RevocationServiceException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message. The detail message is saved for later retrieval by the + * {@link #getMessage()} method. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. + */ + public RevocationServiceException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? null + * : cause.toString()) (which typically contains the class and detail message of cause). This + * constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. + */ + public RevocationServiceException(Throwable cause) { + super(cause); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredential.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredential.java new file mode 100644 index 00000000..654c6dd8 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredential.java @@ -0,0 +1,95 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.jpa; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.OneToOne; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.eclipse.tractusx.managedidentitywallets.revocation.validation.ValidVerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class StatusListCredential { + @Id + @Column(name = "id", length = 256) // Adjust length as per your schema constraints + @NotBlank(message = "ID cannot be blank") + @Size(max = 256, message = "ID cannot exceed 256 characters") + private String id; + + /** + * issuerBpn is a string field that represents the issuer' BPN with the status Purpose at the end + * + *

Example: "BPNL123456789123" + */ + @Column(name = "issuer_bpn", length = 16) // Adjust length as per your schema constraints + @NotBlank(message = "Issuer BPN cannot be blank") + @Size(max = 16, message = "Issuer Bpn cannot exceed 16 characters") + private String issuerBpn; + + // Annotation @Lob indicates that the field should be persisted as a large object to the database. + @Lob + @ValidVerifiableCredential // Custom validation annotation + @NotNull(message = "Credential cannot be null") + @Column(name = "credential", columnDefinition = "TEXT") + @Convert(converter = StringToCredentialConverter.class) + private VerifiableCredential credential; + + @CreationTimestamp + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "modified_at", nullable = false) + private LocalDateTime updatedAt; + + // FetchType.LAZY should be used for large objects as Lob + @OneToOne(mappedBy = "statusListCredential", fetch = FetchType.LAZY) + private StatusListIndex index; + + // Ensure proper validation inside the setter if additional rules are needed + public void setCredential(VerifiableCredential vc) { + if (vc != null) { + credential = vc; + } else { + throw new IllegalArgumentException("Credential cannot be null"); + } + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndex.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndex.java new file mode 100644 index 00000000..7ead8470 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndex.java @@ -0,0 +1,79 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.jpa; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class StatusListIndex { + @Id + @Column(name = "id", length = 256) // Adjust length as per your schema constraints + @NotBlank(message = "ID cannot be blank") + @Size(max = 256, message = "ID cannot exceed 256 characters") + private String id; + + /** + * issuerBpn is a string field that represents the issuer' BPN with the status Purpose at the end + * + *

Example: "issuerBpn-revocation" + */ + @Column(name = "issuer_bpn_status", length = 27) // Adjust length as per your schema constraints + @NotBlank(message = "Issuer BPN with status cannot be blank") + @Size(max = 27, message = "Issuer Bpn with status cannot exceed 27 characters") + private String issuerBpnStatus; + + @Column(name = "current_index", length = 16) // Adjust length as per your schema constraints + @NotBlank(message = "Current index cannot be blank") + @Pattern(regexp = "^[0-9]+$", message = "Current index must be numeric") + @Size(max = 16, message = "Current index cannot exceed 16 characters") + private String currentIndex; + + // Using LAZY fetching strategy to fetch statusListCredential on-demand + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "status_list_credential_id", referencedColumnName = "id") + private StatusListCredential statusListCredential; + + public void setCurrentIndex(String index) { + if (index != null && !index.trim().isEmpty() && index.matches("^[0-9]+$")) { + this.currentIndex = index; + } else { + throw new IllegalArgumentException("Invalid index value"); + } + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StringToCredentialConverter.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StringToCredentialConverter.java new file mode 100644 index 00000000..b4c7c84e --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StringToCredentialConverter.java @@ -0,0 +1,52 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.jpa; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.SneakyThrows; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; + +import java.util.Map; + +@Converter +public class StringToCredentialConverter + implements AttributeConverter { + + private final ObjectMapper objectMapper; + + public StringToCredentialConverter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public String convertToDatabaseColumn(VerifiableCredential attribute) { + return attribute.toJson(); + } + + @SneakyThrows + @Override + public VerifiableCredential convertToEntityAttribute(String data) { + return new VerifiableCredential(objectMapper.readValue(data, Map.class)); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListCredentialRepository.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListCredentialRepository.java new file mode 100644 index 00000000..9ded87a5 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListCredentialRepository.java @@ -0,0 +1,38 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.repository; + +import jakarta.persistence.LockModeType; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListCredential; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface StatusListCredentialRepository + extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + Optional findById(String id); +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListIndexRepository.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListIndexRepository.java new file mode 100644 index 00000000..4a51da96 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/repository/StatusListIndexRepository.java @@ -0,0 +1,37 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.repository; + +import jakarta.persistence.LockModeType; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListIndex; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface StatusListIndexRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + List findByIssuerBpnStatus(String issuerBpnStatus); +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java new file mode 100644 index 00000000..e2fde5e8 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java @@ -0,0 +1,90 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.services; + +import lombok.RequiredArgsConstructor; +import org.eclipse.tractusx.managedidentitywallets.revocation.config.security.SecurityConfigProperties; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.TokenResponse; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@RequiredArgsConstructor +public class HttpClientService { + + private final RestClient restClient = RestClient.create(); + + private final SecurityConfigProperties securityConfigProperties; + + @Value("${revocation.domain.url}") + public String domainUrl; + + @Value("${revocation.miw.url}") + private String miwUrl; + + public String getBearerToken() { + MultiValueMap data = new LinkedMultiValueMap<>(); + data.add("client_id", securityConfigProperties.publicClientId()); + data.add("client_secret", securityConfigProperties.clientId()); + data.add("grant_type", "client_credentials"); + var result = + restClient + .post() + .uri(securityConfigProperties.tokenUrl()) + .accept(MediaType.APPLICATION_FORM_URLENCODED) + .body(data) + .retrieve(); + TokenResponse tokenResponse = result.toEntity(TokenResponse.class).getBody(); + if (tokenResponse == null) { + return null; + } + return tokenResponse.getAccessToken(); + + } + + public VerifiableCredential signStatusListVC(VerifiableCredential vc, String token) { + String uri = + UriComponentsBuilder.fromHttpUrl(miwUrl) + .path("/api/credentials") + .queryParam("isRevocable", "false") + .build() + .toUriString(); + + var result = + restClient + .post() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, token) + .body(vc.toJson()) + .retrieve(); + return result.toEntity(VerifiableCredential.class).getBody(); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java new file mode 100644 index 00000000..1704295b --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java @@ -0,0 +1,365 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.services; + + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.revocation.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; +import org.eclipse.tractusx.managedidentitywallets.revocation.domain.BPN; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusListCredentialSubject; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.BitSetManagerException; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.RevocationServiceException; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListCredential; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListIndex; +import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListCredentialRepository; +import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListIndexRepository; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialBuilder; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The `RevocationService` class is a Java service that handles the revocation of credentials and + * the creation of status lists. + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class RevocationService { + + private final StatusListCredentialRepository statusListCredentialRepository; + + private final StatusListIndexRepository statusListIndexRepository; + + private final HttpClientService httpClientService; + + private final MIWSettings miwSettings; + + /** + * The `revoke` function revokes a credential by updating the status list credential with a new + * subject and saving it to the repository. + * + * @param dto The `dto` parameter is an instance of the `CredentialStatusDto` class. + * @param token the token + * @throws RevocationServiceException the revocation service exception + */ + @Transactional + public void revoke(CredentialStatusDto dto, String token) throws RevocationServiceException { + StatusListCredential statusListCredential; + StatusListCredentialSubject newSubject; + VerifiableCredential statusListVC; + VerifiableCredential signedStatusListVC; + String encodedList; + VerifiableCredentialSubject subjectCredential; + + validateCredentialStatus(dto); + + statusListCredential = + statusListCredentialRepository + .findById(extractIdFromURL(dto.statusListCredential())) + .orElseThrow(() -> new RevocationServiceException("Status list credential not found")); + statusListVC = statusListCredential.getCredential(); + subjectCredential = statusListVC.getCredentialSubject().get(0); + encodedList = (String) subjectCredential.get(StatusListCredentialSubject.SUBJECT_ENCODED_LIST); + String newEncodedList; + try { + newEncodedList = + BitSetManager.revokeCredential(encodedList, Integer.parseInt(dto.statusListIndex())); + } catch (BitSetManagerException e) { + log.error(null, e); + throw new RevocationServiceException(e); + } + newSubject = + StatusListCredentialSubject.builder() + .id((String) subjectCredential.get(StatusListCredentialSubject.SUBJECT_ID)) + .type(StatusListCredentialSubject.TYPE_LIST) + .statusPurpose( + (String) subjectCredential.get(StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE)) + .encodedList(newEncodedList) + .build(); + statusListVC.remove("proof"); + // #TODO credentialSubject should not be a list fix that in SSI LIB + statusListVC.put("credentialSubject", List.of(createCredentialSubject(newSubject))); + signedStatusListVC = httpClientService.signStatusListVC(statusListVC, token); + statusListCredential.setCredential(signedStatusListVC); + log.info("Revoked credential with id:{} , index->{}", dto.id(), dto.statusListIndex()); + statusListCredentialRepository.saveAndFlush(statusListCredential); + } + + /** + * The function creates or updates a status list for a given issuer and purpose, and returns a + * CredentialStatusDto object. + * + * @param dto The parameter "dto" is of type "StatusEntryDto". + * @param token the token + * @return The method is returning a CredentialStatusDto object. + */ + @Transactional + public CredentialStatusDto createStatusList(StatusEntryDto dto, String token) { + StatusListIndex statusListIndex; + String vcUrl; + List statusListIndexs; + String bpn; + + bpn = extractBpnFromDid(dto.issuerId()); + statusListIndexs = + statusListIndexRepository.findByIssuerBpnStatus(bpn + "-" + dto.purpose().toLowerCase()); + statusListIndex = + statusListIndexs.stream() + .filter(li -> Integer.parseInt(li.getCurrentIndex()) + 1 < BitSetManager.BITSET_SIZE) + .findFirst() + .orElseGet( + () -> + createStatusListIndex( + dto, + statusListIndexs.size() + 1, + createStatusListCredential(dto, statusListIndexs.size() + 1, token))); + if (statusListIndex.getCurrentIndex().equals("-1")) { + statusListIndex.setCurrentIndex("0"); + statusListIndexRepository.save(statusListIndex); + log.info("Created new status list for issuer: " + bpn); + } else { + statusListIndex.setCurrentIndex( + String.valueOf(Integer.parseInt(statusListIndex.getCurrentIndex()) + 1)); + statusListIndexRepository.save(statusListIndex); + log.info("Updated status list for issuer: " + bpn); + } + vcUrl = + httpClientService.domainUrl + + RevocationApiEndpoints.REVOCATION_API + + RevocationApiEndpoints.CREDENTIALS_STATUS_INDEX + .replace("{issuerBPN}", bpn) + .replace("{status}", dto.purpose().toLowerCase()) + .replace("{index}", String.valueOf(statusListIndex.getId().split("#")[1])); + return new CredentialStatusDto( + vcUrl + "#" + statusListIndex.getCurrentIndex(), + dto.purpose(), + statusListIndex.getCurrentIndex(), + vcUrl, + StatusListCredentialSubject.TYPE_ENTRY); + } + + + /** + * The function `getStatusLisCredential` retrieves a `VerifiableCredential` object from the + * `statusListCredentialRepository` based on identity + * + * @param issuerBpn the issuer bpn + * @param status the status + * @param index the index + * @return the status list credential + */ + @Transactional + public VerifiableCredential getStatusListCredential( + String issuerBpn, String status, String index) { + return statusListCredentialRepository + .findById(issuerBpn + "-" + status + "#" + index) + .map(StatusListCredential::getCredential) + .orElse(null); + } + + /** + * The function creates a map of key-value pairs representing the properties of a + * StatusListCredentialSubject object. + * + * @param subject The `subject` parameter is an instance of the `StatusListCredentialSubject` + * class. + * @return The method is returning a Map object. + */ + private Map createCredentialSubject(StatusListCredentialSubject subject) { + Map credentialSubjectMap = new HashMap<>(); + credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_ID, subject.getId()); + credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_TYPE, subject.getType()); + credentialSubjectMap.put( + StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE, subject.getStatusPurpose()); + credentialSubjectMap.put( + StatusListCredentialSubject.SUBJECT_ENCODED_LIST, subject.getEncodedList()); + return credentialSubjectMap; + } + + /** + * The function creates a status list credential with a given status entry DTO. + * + * @param dto The "dto" parameter is an object of type "StatusEntryDto". It contains information + * about the status entry, such as the issuer ID and the purpose of the status. + * @return The method `createStatusListCredential` returns a `StatusListCredential` object. + */ + @SneakyThrows + private StatusListCredential createStatusListCredential( + StatusEntryDto dto, Integer size, String token) { + String id; + String bpn; + List types = new ArrayList<>(); + VerifiableCredential statusListVC; + StatusListCredentialSubject subject; + types.add(VerifiableCredentialType.VERIFIABLE_CREDENTIAL); + types.add(StatusListCredentialSubject.TYPE_CREDENTIAL); + + bpn = extractBpnFromDid(dto.issuerId()); + id = + httpClientService.domainUrl + + RevocationApiEndpoints.REVOCATION_API + + RevocationApiEndpoints.CREDENTIALS_STATUS_INDEX + .replace("{issuerBPN}", bpn) + .replace("{status}", dto.purpose().toLowerCase()) + .replace("{index}", String.valueOf(size)); + subject = + StatusListCredentialSubject.builder() + .id(id) + .statusPurpose(dto.purpose().toLowerCase()) + .type(StatusListCredentialSubject.TYPE_LIST) + .encodedList(BitSetManager.initializeEncodedListString()) + .build(); + + // TODO credentialSubject should not be a list fix that in SSI LIB + statusListVC = + new VerifiableCredentialBuilder() + .context(miwSettings.vcContexts()) + .id(URI.create(id)) + .type(types) + .issuer(URI.create(dto.issuerId())) + .issuanceDate(Instant.now()) + .credentialSubject(new VerifiableCredentialSubject(createCredentialSubject(subject))) + .build(); + return StatusListCredential.builder() + .id(bpn + "-" + dto.purpose().toLowerCase() + "#" + size) + .issuerBpn(bpn) + .credential(httpClientService.signStatusListVC(statusListVC, token)) + .build(); + } + + + private StatusListIndex createStatusListIndex( + StatusEntryDto dto, Integer size, StatusListCredential statusListCredential) { + String bpn = extractBpnFromDid(dto.issuerId()); + return StatusListIndex.builder() + .id(bpn + "-" + dto.purpose().toLowerCase() + "#" + size) + .currentIndex("-1") + .statusListCredential(statusListCredential) + .issuerBpnStatus(bpn + "-" + dto.purpose().toLowerCase()) + .build(); + } + + /** + * The function extracts a BPN from a credential status. + * + * @param url The `url` parameter is a string that represents a credential status. + * @return The method is returning a string that is a combination of the first part of the "id" + */ + @SneakyThrows + public String extractBpnFromURL(String url) { + Pattern pattern = Pattern.compile("/credentials/(B\\w+)/", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + return matcher.group(1).toUpperCase(); + } else { + throw new Exception("No match found"); + } + } + + /** + * The function extracts an ID from a status credential id . + * + * @param url The `url` parameter is a string that represents a credential status. + * @return The method is returning a string that is a combination of the first part of the "id" + */ + public String extractIdFromURL(String url) { + Pattern pattern = + Pattern.compile("/credentials/(B\\w+)/(.*?)/(\\d+)", Pattern.CASE_INSENSITIVE); + // Create a Matcher object and find the first match in the URL + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + String bpnlNumber = matcher.group(1); + String purpose = matcher.group(2); + String credentialIndex = matcher.group(3); + return bpnlNumber.toUpperCase() + "-" + purpose + "#" + credentialIndex; + } else { + throw new IllegalArgumentException("No match found"); + } + } + + /** + * The function extracts an ID from a credential status. + * + * @param did The `did` parameter is a string that represents a credential status. + * @return The method is returning a string that is a combination of the first part of the "id" + */ + public String extractBpnFromDid(String did) { + return new BPN(did.substring(did.lastIndexOf(":") + 1).toUpperCase()).value(); + } + + /** + * The function should validate the Credential Status group(1) = BPN Number group(2) = Purpose + * group(3) = status list credential Index group(4) = index of the verifiable credential in the + * status list + * + * @param dto the dto + * @throws IllegalArgumentException if the Credential Status is invalid + */ + public void validateCredentialStatus(CredentialStatusDto dto) { + String domainUrl = httpClientService.domainUrl + RevocationApiEndpoints.REVOCATION_API; + String urlPattern = domainUrl + "/credentials/(B\\w+)/(.*?)/(\\d+)"; + Pattern pattern0 = Pattern.compile(urlPattern, Pattern.CASE_INSENSITIVE); + Pattern pattern1 = Pattern.compile(urlPattern + "#(\\d+)", Pattern.CASE_INSENSITIVE); + Matcher matcher0 = pattern0.matcher(dto.statusListCredential()); + Matcher matcher1 = pattern1.matcher(dto.id()); + if (!matcher0.find()) { + throw new IllegalArgumentException("Invalid credential status url"); + } + if (!matcher1.find()) { + throw new IllegalArgumentException("Invalid credential status id"); + } + + if (!matcher0.group(1).equals(matcher1.group(1)) + || !matcher0.group(2).equals(matcher1.group(2)) + || !matcher0.group(3).equals(matcher1.group(3))) { + throw new IllegalArgumentException("Credential status url and id do not match"); + } + + if (!matcher1.group(4).equals(dto.statusListIndex())) { + throw new IllegalArgumentException( + "Credential status index in the id does not match the current index in the dto"); + } + + if (!matcher0.group(2).equals(dto.statusPurpose())) { + throw new IllegalArgumentException( + "Credential status purpose does not match the statusPurpose in the dto"); + } + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManager.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManager.java new file mode 100644 index 00000000..a195a358 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManager.java @@ -0,0 +1,105 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.utils; + +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.BitSetManagerException; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.CredentialAlreadyRevokedException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.BitSet; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public class BitSetManager { + + public static final int BITSET_SIZE = 131072; + + private BitSetManager() { + // static methods only + } + + public static String initializeEncodedListString() { + BitSet bitSet = new BitSet(BITSET_SIZE); + byte[] compressedBitSet = compress(bitSet); + return encodeToString(compressedBitSet); + } + + public static byte[] compress(BitSet bitSet) { + byte[] byteArray = bitSet.toByteArray(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); + gzipOutputStream.write(byteArray); + gzipOutputStream.close(); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new BitSetManagerException(e); + } + } + + public static String revokeCredential(String encodedList, int index) + throws BitSetManagerException { + BitSet bitSet = decompress(decodeFromString(encodedList)); + if (bitSet.get(index)) { + throw new CredentialAlreadyRevokedException("Credential already revoked"); + } + bitSet.set(index); + byte[] compressedBitSet = compress(bitSet); + return encodeToString(compressedBitSet); + } + + public static String suspendCredential(String encodedList, int index) { + BitSet bitSet = decompress(decodeFromString(encodedList)); + bitSet.flip(index); + byte[] compressedBitSet = compress(bitSet); + return encodeToString(compressedBitSet); + } + + public static BitSet decompress(byte[] data) { + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) { + byte[] buffer = new byte[1024]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int len; + while ((len = gzipInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, len); + } + BitSet bitSet = BitSet.valueOf(outputStream.toByteArray()); + outputStream.close(); + return bitSet; + + } catch (IOException e) { + throw new BitSetManagerException(e); + } + } + + public static String encodeToString(byte[] data) { + return Base64.getEncoder().encodeToString(data); + } + + public static byte[] decodeFromString(String data) { + return Base64.getDecoder().decode(data); + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java new file mode 100644 index 00000000..1aeb1f42 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java @@ -0,0 +1,27 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.utils; + +public class StringPool { + + public static final String BPN = "bpn"; +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java new file mode 100644 index 00000000..724153dc --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java @@ -0,0 +1,153 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.utils; + +import java.util.Objects; + +/** + * The type Validate. + * + * @param the type parameter + */ +public class Validate { + private T value; + private boolean match = false; + + private Validate() { + } + + private Validate(T value) { + this.value = value; + } + + /** + * Value validate. + * + * @param the type parameter + * @param value the value + * @return the validate + */ + public static Validate value(V value) { + return new Validate<>(value); + } + + /** + * Is true validate. + * + * @param the type parameter + * @param condition the condition + * @return the validate + */ + public static Validate isTrue(boolean condition) { + Validate validate = new Validate<>(); + if (condition) { + validate.match = true; + } + return validate; + } + + /** + * Throws if {@code condition} is false + * + * @param the type parameter + * @param condition the condition + * @return validate validate + */ + public static Validate isFalse(boolean condition) { + Validate validate = new Validate<>(); + if (!condition) { + validate.match = true; + } + return validate; + } + + /** + * Is null validate. + * + * @param the type parameter + * @param value the value + * @return the validate + */ + public static Validate isNull(T value) { + return new Validate<>(value).isNull(); + } + + /** + * Is not null validate. + * + * @param the type parameter + * @param value the value + * @return the validate + */ + public static Validate isNotNull(T value) { + return new Validate<>(value).isNotNull(); + } + + /** + * Is not empty validate. + * + * @return the validate + */ + public Validate isNotEmpty() { + if (match || Objects.isNull(value) || String.valueOf(value).trim().isEmpty()) { + match = true; + } + return this; + } + + /** + * Is null validate. + * + * @return the validate + */ + public Validate isNull() { + if (match || Objects.isNull(value)) { + match = true; + } + return this; + } + + /** + * Is not null validate. + * + * @return the validate + */ + public Validate isNotNull() { + if (match || !Objects.isNull(value)) { + match = true; + } + return this; + } + + /** + * Throw passed exception if expression is match + * + * @param e exception to throw + * @return the t + */ + public T launch(RuntimeException e) { + if (match) { + throw e; + } + return value; + } +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/ValidVerifiableCredential.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/ValidVerifiableCredential.java new file mode 100644 index 00000000..2df9f535 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/ValidVerifiableCredential.java @@ -0,0 +1,43 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = VerifiableCredentialValidator.class) +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidVerifiableCredential { + String message() default "Invalid Verifiable Credential data"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java new file mode 100644 index 00000000..66372d57 --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java @@ -0,0 +1,95 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.NonNull; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +public class VerifiableCredentialValidator + implements ConstraintValidator { + + @Override + public void initialize(ValidVerifiableCredential constraintAnnotation) { + // You can add initialization logic here if needed. + } + + @Override + public boolean isValid(VerifiableCredential credential, ConstraintValidatorContext context) { + if (credential == null) { + return true; // @NotNull should handle null cases + } + + // Assuming 'id' within 'credentialSubject' is the field to be validated as a URI + @NonNull + List credentialSubject = credential.getCredentialSubject(); + if (credentialSubject != null) { + if (!validateCredentialSubject(credentialSubject, context)) { + return false; + } + } + + // Additional validation checks can be added here, e.g., checking the proof object + + return true; + } + + private boolean validateCredentialSubject( + List credentialSubjects, ConstraintValidatorContext context) { + // We iterate over the list of credential subjects to validate each one + for (Map subject : credentialSubjects) { + // Extract the 'id' of the credential subject if it exists + Object subjectId = subject.get("id"); + if (subjectId == null || !(subjectId instanceof String)) { + addConstraintViolation(context, "credentialSubject.id must be a valid String"); + return false; + } + + // Check for a valid URI in the subject ID + if (!isValidUri((String) subjectId)) { + addConstraintViolation(context, "credentialSubject.id must be a valid URI"); + return false; + } + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message).addConstraintViolation(); + } + + private boolean isValidUri(String uriStr) { + try { + URI uri = new URI(uriStr); + return uri.isAbsolute(); + } catch (Exception e) { + return false; + } + } +} diff --git a/revocation-service/src/main/resources/application.yaml b/revocation-service/src/main/resources/application.yaml new file mode 100644 index 00000000..98f4296f --- /dev/null +++ b/revocation-service/src/main/resources/application.yaml @@ -0,0 +1,130 @@ +################################################################################ +# Copyright (c) 2021,2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +#set all env here +revocation: + application: + name: ${APPLICATION_NAME:Revocation} + port: ${APPLICATION_PORT:8080} + profile: ${APPLICATION_PROFILE:local} + database: + host: ${DATABASE_HOST:localhost} + port: ${DATABASE_PORT:5434} + name: ${DATABASE_NAME:revocation} + useSSL: ${DATABASE_USE_SSL_COMMUNICATION:false} + username: ${DATABASE_USERNAME:admin} + password: ${DATABASE_PASSWORD:admin} + connectionPoolSize: ${DATABASE_CONNECTION_POOL_SIZE:10} + swagger: + enableSwaggerUi: ${ENABLE_SWAGGER_UI:true} + enableApiDoc: ${ENABLE_API_DOC:true} + config: + applicationLogLevel: ${APPLICATION_LOG_LEVEL:DEBUG} + security: + service: + enabled: ${SERVICE_SECURITY_ENABLED:true} + keycloak: + enabled: ${KEYCLOAK_ENABLED:true} + realm: ${KEYCLOAK_REALM:miw_test} + clientId: ${KEYCLOAK_CLIENT_ID:miw_private_client} + publicClientId: ${KEYCLOAK_PUBLIC_CLIENT_ID:miw_public_client} #used for swagger login + auth-server-url: ${AUTH_SERVER_URL:http://localhost:28080} + auth-url: ${revocation.security.keycloak.auth-server-url}/realms/${revocation.security.keycloak.realm}/protocol/openid-connect/auth + token-url: ${revocation.security.keycloak.auth-server-url}/realms/${revocation.security.keycloak.realm}/protocol/openid-connect/token + refresh-token-url: ${revocation.security.keycloak.token-url} + miw: + url: ${MIW_URL:https://b366-203-129-213-107.ngrok-free.app} + vcContexts: ${VC_SCHEMA_LINK:https://www.w3.org/2018/credentials/v1, https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json} + domain: + url: ${DOMAIN_URL:http://localhost:8080} + + + +#Spring boot configuration +server: + port: ${revocation.application.port} + shutdown: graceful + compression: + enabled: true +spring: + profiles: + active: ${revocation.application.profile} + liquibase: + change-log: classpath:/db/changelog/changelog-master.xml + application: + name: revocation + datasource: + url: jdbc:postgresql://${revocation.database.host}:${revocation.database.port}/${revocation.database.name}?useSSL=${revocation.database.useSSL} + username: ${revocation.database.username} + password: ${revocation.database.password} + initialization-mode: always + hikari: + maximumPoolSize: ${revocation.database.connectionPoolSize} + jpa: + show-sql: false + security: + oauth2: + resourceserver: + jwt: + #Issuer-uri indicates the iss claims from jwt token + issuer-uri: ${revocation.security.keycloak.auth-server-url}/realms/${revocation.security.keycloak.realm} + jwk-set-uri: ${revocation.security.keycloak.auth-server-url}/realms/${revocation.security.keycloak.realm}/protocol/openid-connect/certs + +## only needed if you want to enable API doc +springdoc: + swagger-ui: + enabled: ${revocation.swagger.enableSwaggerUi} + oauth: + clientId: ${revocation.security.keycloak.publicClientId} #It should be public client created in keycloak + disable-swagger-default-url: true + path: /ui/swagger-ui + show-common-extensions: true + csrf: + enabled: true + api-docs: + enabled: ${revocation.swagger.enableApiDoc} + path: /docs/api-docs + +management: + endpoint: + loggers: + enabled: true + health: + probes: + enabled: true + endpoints: + web: + exposure: + include: '*, pre-stop, loggers' + health: + db: + enabled: true + livenessState: + enabled: true + readinessState: + enabled: true + +# log level +logging: + level: + org: + eclipse: + tractusx: + managedidentitywallets: + revocation: ${APP_LOG_LEVEL:INFO} diff --git a/revocation-service/src/main/resources/db/changelog/changelog-master.xml b/revocation-service/src/main/resources/db/changelog/changelog-master.xml new file mode 100755 index 00000000..01cb2640 --- /dev/null +++ b/revocation-service/src/main/resources/db/changelog/changelog-master.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/revocation-service/src/main/resources/db/changelog/changes/init.sql b/revocation-service/src/main/resources/db/changelog/changes/init.sql new file mode 100755 index 00000000..696ad8b2 --- /dev/null +++ b/revocation-service/src/main/resources/db/changelog/changes/init.sql @@ -0,0 +1,69 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +-- liquibase formatted sql + +-- changeset pmanaras:1.0 dbms:postgresql +CREATE TABLE status_list_credential ( + id VARCHAR(256) NOT NULL, + issuer_bpn VARCHAR(16), + credential TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + modified_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + CONSTRAINT pk_statuslistcredential PRIMARY KEY (id) +); + +CREATE TABLE status_list_index ( + id VARCHAR(256) NOT NULL, + issuer_bpn_status VARCHAR(27), + current_index VARCHAR(16), + status_list_credential_id VARCHAR(256), + CONSTRAINT pk_statuslistindex PRIMARY KEY (id) +); + +ALTER TABLE status_list_index ADD CONSTRAINT uc_statuslistindex_status_list_credential UNIQUE (status_list_credential_id); + +ALTER TABLE status_list_index ADD CONSTRAINT FK_STATUSLISTINDEX_ON_STATUS_LIST_CREDENTIAL FOREIGN KEY (status_list_credential_id) REFERENCES status_list_credential (id); + +-- changeset pmanaras:1.0 dbms:h2 +CREATE TABLE IF NOT EXISTS status_list_credential ( + id VARCHAR(256) NOT NULL, + issuer_bpn VARCHAR(16), + credential CLOB NOT NULL, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP NOT NULL, + CONSTRAINT pk_statuslistcredential PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS status_list_index ( + id VARCHAR(256) NOT NULL, + issuer_bpn_status VARCHAR(27), + current_index VARCHAR(16), + status_list_credential_id VARCHAR(256), + CONSTRAINT pk_statuslistindex PRIMARY KEY (id) +); + +ALTER TABLE status_list_index DROP CONSTRAINT IF EXISTS uc_statuslistindex_status_list_credential; +ALTER TABLE status_list_index DROP CONSTRAINT IF EXISTS FK_STATUSLISTINDEX_ON_STATUS_LIST_CREDENTIAL; + +ALTER TABLE status_list_index ADD CONSTRAINT uc_statuslistindex_status_list_credential UNIQUE (status_list_credential_id); + +ALTER TABLE status_list_index ADD CONSTRAINT FK_STATUSLISTINDEX_ON_STATUS_LIST_CREDENTIAL FOREIGN KEY (status_list_credential_id) REFERENCES status_list_credential (id); diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java new file mode 100644 index 00000000..bbb67685 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java @@ -0,0 +1,189 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation; + +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusListCredentialSubject; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListCredential; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListIndex; +import org.eclipse.tractusx.ssi.lib.crypt.KeyPair; +import org.eclipse.tractusx.ssi.lib.crypt.x25519.X25519Generator; +import org.eclipse.tractusx.ssi.lib.exception.json.TransformJsonLdException; +import org.eclipse.tractusx.ssi.lib.exception.key.InvalidPrivateKeyFormatException; +import org.eclipse.tractusx.ssi.lib.exception.key.KeyGenerationException; +import org.eclipse.tractusx.ssi.lib.exception.proof.SignatureGenerateFailedException; +import org.eclipse.tractusx.ssi.lib.exception.proof.UnsupportedSignatureTypeException; +import org.eclipse.tractusx.ssi.lib.model.proof.Proof; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialBuilder; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; +import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofGenerator; +import org.eclipse.tractusx.ssi.lib.proof.SignatureType; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.BitSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static org.mockito.Mockito.when; + +public class TestUtil { + + public static final int BITSET_SIZE = 131072; + + public static final String STATUS_LIST_ID = "https://example.com/credentials/status/3"; + + public static final String BPN = "BPNL123456789000"; + + public static final String DID = "did:web:example:BPNL123456789000"; + + public static final List VC_CONTEXTS = + List.of(URI.create("https://w3id.org/vc/status-list/2021/v1")); + + public static final String STATUS_LIST_CREDENTIAL_SUBJECT_ID = + "https://example.com/status/3#list"; + + public static StatusListIndex mockStatusListIndex( + String issuerBpnStatus, StatusListCredential statusListCredential, String currentIndex) { + var statusListIndex = Mockito.mock(StatusListIndex.class); + when(statusListIndex.getStatusListCredential()).thenReturn(statusListCredential); + when(statusListIndex.getCurrentIndex()).thenReturn(currentIndex); + when(statusListIndex.getIssuerBpnStatus()).thenReturn(issuerBpnStatus); + return statusListIndex; + } + + public static String mockEmptyEncodedList() { + BitSet bitSet = new BitSet(BITSET_SIZE); + return Base64.getEncoder().encodeToString(gzipCompress(bitSet)); + } + + public static VerifiableCredentialBuilder mockStatusListVC( + String issuer, String index, String encodedList) { + var builder = + new VerifiableCredentialBuilder() + .context(VC_CONTEXTS) + .id(URI.create(issuer + "#" + index)) + .type(List.of("VerifiableCredential", "BitstringStatusListCredential")) + .issuer(URI.create(issuer)) + .expirationDate(Instant.now().plusSeconds(200000000L)) + .issuanceDate(Instant.now()) + .credentialSubject(new VerifiableCredentialSubject(mockStatusList(encodedList))); + return builder; + } + + public static StatusListCredential mockStatusListCredential( + String issuer, VerifiableCredentialBuilder builder) { + + final LinkedDataProofGenerator generator; + try { + generator = LinkedDataProofGenerator.newInstance(SignatureType.ED25519); + } catch (UnsupportedSignatureTypeException e) { + throw new AssertionError(e); + } + + final Proof proof; + try { + proof = + generator.createProof( + builder.build(), URI.create(issuer + "#key-1"), generateKeys().getPrivateKey()); + } catch (InvalidPrivateKeyFormatException + | SignatureGenerateFailedException + | TransformJsonLdException e) { + throw new AssertionError(e); + } + + // Adding Proof to VC + builder.proof(proof); + + StatusListCredential statusListCredential = Mockito.mock(StatusListCredential.class); + when(statusListCredential.getCredential()).thenReturn(builder.build()); + when(statusListCredential.getId()).thenReturn(STATUS_LIST_ID); + when(statusListCredential.getIssuerBpn()).thenReturn(issuer); + when(statusListCredential.getCreatedAt()).thenReturn(LocalDateTime.now()); + + return statusListCredential; + } + + public static KeyPair generateKeys() { + X25519Generator gen = new X25519Generator(); + KeyPair baseWalletKeys; + try { + baseWalletKeys = gen.generateKey(); + } catch (KeyGenerationException e) { + throw new AssertionError(e); + } + return baseWalletKeys; + } + + public static Map mockStatusList(String encodedList) { + Map credentialSubjectMap = new HashMap(); + credentialSubjectMap.put( + StatusListCredentialSubject.SUBJECT_ID, STATUS_LIST_CREDENTIAL_SUBJECT_ID); + credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_TYPE, "BitstringStatusList"); + credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE, "revocation"); + credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_ENCODED_LIST, encodedList); + return credentialSubjectMap; + } + + public static byte[] gzipCompress(BitSet bitSet) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (GZIPOutputStream gzipOut = new GZIPOutputStream(out)) { + gzipOut.write(bitSet.toByteArray()); + } catch (IOException e) { + throw new AssertionError("failed."); + } + + return out.toByteArray(); + } + + public static BitSet decompressGzip(byte[] bytes) { + ByteArrayInputStream in = new ByteArrayInputStream(bytes); + try (GZIPInputStream gzipIn = new GZIPInputStream(in)) { + byte[] buffer = new byte[1024]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + int len; + while ((len = gzipIn.read(buffer)) > 0) { + outputStream.write(buffer, 0, len); + } + + BitSet bitSet = BitSet.valueOf(outputStream.toByteArray()); + outputStream.close(); + return bitSet; + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String extractBpnFromDid(String did) { + return did.substring(did.lastIndexOf(":") + 1).toUpperCase(); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestVerifiableCredentialsRevocationServiceApplication.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestVerifiableCredentialsRevocationServiceApplication.java new file mode 100644 index 00000000..e7d5f139 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestVerifiableCredentialsRevocationServiceApplication.java @@ -0,0 +1,45 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +@TestConfiguration(proxyBeanMethods = false) +public class TestVerifiableCredentialsRevocationServiceApplication { + + public static void main(String[] args) { + SpringApplication.from(VerifiableCredentialsRevocationServiceApplication::main) + .with(TestVerifiableCredentialsRevocationServiceApplication.class) + .run(args); + } + + @Bean + @ServiceConnection + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest")); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java new file mode 100644 index 00000000..9e4e07c0 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java @@ -0,0 +1,73 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.CredentialAlreadyRevokedException; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.ForbiddenException; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ExceptionHandlingTest { + + private static final ExceptionHandling exceptionHandling = new ExceptionHandling(); + + @Test + void handleCredentialAlreadyRevokedException() { + CredentialAlreadyRevokedException credentialAlreadyRevokedException = + new CredentialAlreadyRevokedException("credential xyz was already revoked"); + ProblemDetail problemDetail = + exceptionHandling.handleCredentialAlreadyRevokedException( + credentialAlreadyRevokedException); + + assertNotNull(problemDetail); + assertNotNull(problemDetail.getTitle()); + assertEquals("Revocation service error", problemDetail.getTitle()); + assertEquals(HttpStatus.CONFLICT.value(), problemDetail.getStatus()); + } + + @Test + void handleForbiddenException() { + ForbiddenException forbiddenException = new ForbiddenException("no!"); + ProblemDetail problemDetail = exceptionHandling.handleForbiddenException(forbiddenException); + + assertNotNull(problemDetail); + assertNotNull(problemDetail.getTitle()); + assertEquals("ForbiddenException: no!", problemDetail.getTitle()); + assertEquals(HttpStatus.FORBIDDEN.value(), problemDetail.getStatus()); + } + + @Test + void handleIllegalArgumentException() { + IllegalArgumentException illegalArgumentException = new IllegalArgumentException("illegal"); + ProblemDetail problemDetail = + exceptionHandling.handleIllegalArgumentException(illegalArgumentException); + + assertNotNull(problemDetail); + assertNotNull(problemDetail.getTitle()); + assertEquals("IllegalArgumentException: illegal", problemDetail.getTitle()); + assertEquals(HttpStatus.BAD_REQUEST.value(), problemDetail.getStatus()); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java new file mode 100644 index 00000000..c04163e8 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java @@ -0,0 +1,55 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MIWSettingsTest { + + @Test + void testMIWSettingsWithValidData() { + List vcContexts = + Arrays.asList( + URI.create("https://example.com/context1"), URI.create("https://example.com/context2")); + + MIWSettings miwSettings = new MIWSettings(vcContexts); + + assertEquals(vcContexts, miwSettings.vcContexts()); + } + + @Test + void testMIWSettingsWithNullVCContexts() { + assertThrows(NullPointerException.class, () -> new MIWSettings(null)); + } + + @Test + void testMIWSettingsWithEmptyVCContexts() { + assertThrows(IllegalArgumentException.class, () -> new MIWSettings(List.of())); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfigTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfigTest.java new file mode 100644 index 00000000..db45d705 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/OpenApiConfigTest.java @@ -0,0 +1,73 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config; + +import io.swagger.v3.oas.models.OpenAPI; +import org.eclipse.tractusx.managedidentitywallets.revocation.config.security.SecurityConfigProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springdoc.core.models.GroupedOpenApi; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +class OpenApiConfigTest { + + private static SecurityConfigProperties securityConfigProperties; + + private static OpenApiConfig openApiconfig; + + @BeforeAll + public static void beforeAll() { + securityConfigProperties = Mockito.mock(SecurityConfigProperties.class); + openApiconfig = new OpenApiConfig(securityConfigProperties); + } + + @Test + void testOpenApiSecurityEnabled() { + when(securityConfigProperties.enabled()).thenReturn(true); + OpenAPI openAPI = assertDoesNotThrow(() -> openApiconfig.openAPI()); + + assertFalse(openAPI.getSecurity().isEmpty()); + openAPI + .getSecurity() + .forEach(s -> assertTrue(s.containsKey("Authenticate using access_token"))); + } + + @Test + void testOpenApiSecurityDisabled() { + when(securityConfigProperties.enabled()).thenReturn(false); + OpenAPI openAPI = assertDoesNotThrow(() -> openApiconfig.openAPI()); + assertNull(openAPI.getSecurity()); + } + + @Test + void testOpenApiDefinition() { + GroupedOpenApi groupedOpenApi = assertDoesNotThrow(() -> openApiconfig.openApiDefinition()); + assertNotNull(groupedOpenApi); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java new file mode 100644 index 00000000..29de8663 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java @@ -0,0 +1,111 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.config.security; + +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CustomAuthenticationConverterTest { + + private static String VALID = + "eyJhbGciOiJSUzI1NiIsImFscGhhIjoiZzB1ZjNycjlycnN2cHlhcTVuamg4In0.eyJpc3MiOiJEaW5vQ2hpZXNhLmdpdGh1Yi5pbyIsInN1YiI6Im1heGluZSIsImF1ZCI6ImlkcmlzIiwiaWF0IjoxNzAyNjUwMTc2LCJleHAiOjE3MDI2NTA3NzYsInJlc291cmNlX2FjY2VzcyI6eyJyZXNvdXJjZUlkIjp7InJvbGVzIjpbImRlaV9tdWRhIl19fX0.wTv9GBX3AuRO8UIsAuu2YJU77ai-wchDyxRn-_yX9PeHt23vCmp_JAbkkdMdyLAWWOKncjgNeG-4lB9RCBsjmbdb1imujUrAocp3VZQqNg6OVaNV58kdsIpNNF9S8XlFI4hr1BANrw2rWJDkTRu1id-Fu-BVE1BF7ySCKHS_NaY3e7yXQM-jtU63z5FBpPvfMF-La3blPle93rgut7V3LlG-tNOp93TgFzGrQQXuJUsew34T0u4OlQa3TjQuMdZMTy0SVSLSpIzAqDsAkHv34W6SdY1p6FVQ14TfawRLkrI2QY-YM_dCFAEE7KqqnUrVVyw6XG1ydeFDuX8SJuQX7g"; + + private static String MISSING_RESOURCE_ID = + "{\n" + " \"resource_access\": {\n" + " }\n" + "}"; + + private static String MISSING_ROLES = + "{\n" + + " \"resource_access\": {\n" + + " \"resourceId\": {\n" + + " }\n" + + " }\n" + + "}"; + + @Test + void shouldConvertSuccessfullyWithAuthorities() { + + Map roles = Map.of("roles", List.of("dei_muda")); + Map resourceId = Map.of("resourceId", roles); + Map resourceAccess = Map.of("resource_access", resourceId); + + Jwt jwt = + new Jwt( + "32453453", + Instant.now(), + Instant.now().plus(Duration.ofDays(12)), + Map.of("kid", "1234"), + resourceAccess); + + CustomAuthenticationConverter converter = new CustomAuthenticationConverter("resourceId"); + AbstractAuthenticationToken abstractAuthenticationToken = + assertDoesNotThrow(() -> converter.convert(jwt)); + assertFalse(abstractAuthenticationToken.getAuthorities().isEmpty()); + } + + @Test + void shouldConvertSuccessfullyWithoutAuthoritiesWhenRolesMissing() { + Map resourceId = Map.of("resourceId", Map.of()); + Map resourceAccess = Map.of("resource_access", resourceId); + + Jwt jwt = + new Jwt( + "32453453", + Instant.now(), + Instant.now().plus(Duration.ofDays(12)), + Map.of("kid", "1234"), + resourceAccess); + + CustomAuthenticationConverter converter = new CustomAuthenticationConverter("resourceId"); + AbstractAuthenticationToken abstractAuthenticationToken = + assertDoesNotThrow(() -> converter.convert(jwt)); + assertTrue(abstractAuthenticationToken.getAuthorities().isEmpty()); + } + + @Test + void shouldConvertSuccessfullyWithoutAuthoritiesWhenResourceAccessMissing() { + Map resourceId = Map.of("resourceId", Map.of()); + Map resourceAccess = Map.of("resource_access", Map.of()); + + Jwt jwt = + new Jwt( + "32453453", + Instant.now(), + Instant.now().plus(Duration.ofDays(12)), + Map.of("kid", "1234"), + resourceAccess); + + CustomAuthenticationConverter converter = new CustomAuthenticationConverter("resourceId"); + AbstractAuthenticationToken abstractAuthenticationToken = + assertDoesNotThrow(() -> converter.convert(jwt)); + assertTrue(abstractAuthenticationToken.getAuthorities().isEmpty()); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java new file mode 100644 index 00000000..4d0740c7 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java @@ -0,0 +1,223 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.services.RevocationService; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.StringPool; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.security.Principal; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BPN; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.DID; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class RevocationApiControllerTest { + + private static final String CALLER_BPN = UUID.randomUUID().toString(); + + private MockMvc mockMvc; + + private ObjectMapper objectMapper; + + @Mock + private RevocationService revocationService; + + @InjectMocks + private RevocationApiController revocationApiController; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders.standaloneSetup(revocationApiController).build(); + objectMapper = new ObjectMapper(); + Mockito.reset(revocationService); + } + + @Test + public void whenPostCreateStatusListVC_thenReturnStatus() throws Exception { + // Given + String validPurpose = PurposeType.REVOCATION.toString(); + StatusEntryDto statusEntryDto = new StatusEntryDto(validPurpose, DID); + String validIndex = + String.valueOf(BitSetManager.BITSET_SIZE / 2); // any valid index within range + CredentialStatusDto credentialStatusDto = + new CredentialStatusDto( + "https://example.com/revocations/credentials/" + BPN + "/revocation/1#" + validIndex, + PurposeType.REVOCATION.toString(), + validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] + "https://example.com/revocations/credentials/" + BPN + "/revocation/1", + "BitstringStatusListEntry"); + given(revocationService.createStatusList(statusEntryDto, "token")) + .willReturn(credentialStatusDto); + when(revocationService.extractBpnFromDid(DID)).thenReturn(BPN); + + Principal mockPrincipal = mockPrincipal(BPN); + var name = mockPrincipal.getName(); + // When & Then + mockMvc + .perform( + MockMvcRequestBuilders.post(RevocationApiEndpoints.REVOCATION_API + RevocationApiEndpoints.STATUS_ENTRY) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "token") + .principal(mockPrincipal) + .content(objectMapper.writeValueAsString(statusEntryDto))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(credentialStatusDto.id())); + } + + private Principal mockPrincipal(String name) { + + Jwt jwt = mock(Jwt.class); + when(jwt.getClaims()).thenReturn(Map.of(StringPool.BPN, BPN)); + + JwtAuthenticationToken principal = Mockito.mock(JwtAuthenticationToken.class); + when(principal.getName()).thenReturn(name); + when(principal.getPrincipal()).thenReturn(jwt); + + return principal; + } + + @Test + public void whenPostRevokeCredential_thenReturnOkStatus() throws Exception { + // Given + String validIndex = + String.valueOf(BitSetManager.BITSET_SIZE / 2); // any valid index within range + CredentialStatusDto credentialStatusDto = + new CredentialStatusDto( + "http://example.com/credentials/" + BPN + "/revocation/1#" + validIndex, + "revocation", + validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] + "http://example.com/credentials/" + BPN + "/revocation/1", + "BitstringStatusListEntry"); + doNothing().when(revocationService).revoke(credentialStatusDto, "token"); + when(revocationService.extractBpnFromURL(any())).thenReturn(BPN); + + Principal mockPrincipal = mockPrincipal(BPN); + // When & Then + mockMvc + .perform( + MockMvcRequestBuilders.post(RevocationApiEndpoints.REVOCATION_API + RevocationApiEndpoints.REVOKE) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "token") + .principal(mockPrincipal) + .content(objectMapper.writeValueAsString(credentialStatusDto))) + .andExpect(status().isOk()); + verify(revocationService).revoke(credentialStatusDto, "token"); + } + + @Test + public void whenGetCredential_thenReturnCredentials() throws Exception { + // Given + String validPurpose = PurposeType.REVOCATION.toString(); + VerifiableCredential verifiableCredential = + new VerifiableCredential( + createVerifiableCredentialTestData()); // Populate with valid test data + given(revocationService.getStatusListCredential(any(), any(), any())) + .willReturn(verifiableCredential); + // When & Then + mockMvc + .perform( + MockMvcRequestBuilders.get( + RevocationApiEndpoints.REVOCATION_API + + RevocationApiEndpoints.CREDENTIALS_STATUS_INDEX + .replace("{issuerBPN}", BPN) + .replace("{status}", validPurpose) + .replace("{index}", "1"))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.id").value(verifiableCredential.getId().toString())); + } + + private VerifiableCredential createVerifiableCredentialTestData() { + Map credentialData = new HashMap<>(); + credentialData.put( + "id", "http://example/api/v1/revocations/credentials/" + BPN + "/revocation/1"); + credentialData.put("issuer", "https://issuer.example.com"); + credentialData.put("issuanceDate", Instant.now().toString()); + // Include 'type' field as a list because VerifiableCredential expects it to be non-null and a + // list + credentialData.put("type", List.of("VerifiableCredential", "StatusListCredential")); + Map subjectData = new HashMap<>(); + subjectData.put("id", "subjectId"); + subjectData.put("type", "StatusList2021Credential"); + // 'credentialSubject' can be either a List or a single Map according to the code, so I'm + // keeping it as a single Map + credentialData.put("credentialSubject", subjectData); + credentialData.put("@context", VerifiableCredential.DEFAULT_CONTEXT.toString()); + VerifiableCredential credential = new VerifiableCredential(credentialData); + return credential; + } + + private VerifiableCredential createVerifiableCredentialTestDataInvalidDID() { + Map credentialData = new HashMap<>(); + credentialData.put("id", UUID.randomUUID().toString()); + credentialData.put("issuer", "https://issuer.example.com"); + credentialData.put("issuanceDate", Instant.now().toString()); + // Include 'type' field as a list because VerifiableCredential expects it to be non-null and a + // list + credentialData.put("type", List.of("VerifiableCredential", "StatusListCredential")); + Map subjectData = new HashMap<>(); + subjectData.put("id", "subjectId"); + subjectData.put("type", "StatusList2021Credential"); + // 'credentialSubject' can be either a List or a single Map according to the code, so I'm + // keeping it as a single Map + credentialData.put("credentialSubject", subjectData); + credentialData.put("@context", VerifiableCredential.DEFAULT_CONTEXT.toString()); + VerifiableCredential credential = new VerifiableCredential(credentialData); + return credential; + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java new file mode 100644 index 00000000..cdec1660 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java @@ -0,0 +1,79 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BPN; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BPNTest { + + @Test + @DisplayName("BPN Should not be valid") + public void invalidBPN() { + String bpn = "thisnotbpn"; + + assertThrows( + IllegalArgumentException.class, + () -> { + new BPN(bpn); + }); + } + + @Test + @DisplayName("BPN Should be valid") + public void validBPN() { + assertDoesNotThrow( + () -> { + new BPN(BPN); + }); + } + + @Test + @DisplayName("BPN Should return value") + public void bpnValue() { + BPN bpn = new BPN(BPN); + assertEquals(BPN, bpn.value()); + } + + @Test + @DisplayName("BPN Should be equal") + public void bpnEqual() { + BPN bpn1 = new BPN(BPN); + BPN bpn2 = new BPN(BPN); + assertTrue(bpn1.equals(bpn2)); + } + + @Test + @DisplayName("BPN Should not be equal") + public void bpnNotEqual() { + BPN bpn1 = new BPN(BPN); + BPN bpn2 = new BPN("BPNL000000000000"); + assertFalse(bpn1.equals(bpn2)); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java new file mode 100644 index 00000000..43235a50 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java @@ -0,0 +1,154 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CredentialStatusDtoTest { + + @Test + void validCredentialStatusDto_CreatesSuccessfully() { + // Arrange + String validIndex = + String.valueOf(BitSetManager.BITSET_SIZE / 2); // any valid index within range + + // Act + CredentialStatusDto dto = + new CredentialStatusDto( + "id", + "revocation", + validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] + "statusListCredential", + "BitstringStatusListEntry"); + + // Assert + assertNotNull(dto); + assertEquals("id", dto.id()); + assertEquals("revocation", dto.statusPurpose()); + assertEquals(validIndex, dto.statusListIndex()); + assertEquals("statusListCredential", dto.statusListCredential()); + assertEquals("BitstringStatusListEntry", dto.type()); + } + + @ParameterizedTest + @ValueSource(ints = { BitSetManager.BITSET_SIZE, -6 }) + void statusListIndexOutOfRange_ThrowsIllegalArgumentException(int value) { + // Arrange + String outOfRangeIndex = String.valueOf(value); // one more than the max index + + // Act & Assert + assertThrows( + IllegalArgumentException.class, + () -> { + new CredentialStatusDto( + "id", "statusPurpose", outOfRangeIndex, "statusListCredential", "type"); + }); + } + + @Test + void anyParameterIsBlank_ThrowsValidationException() { + + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + assertFalse( + validator + .validate( + new CredentialStatusDto( + "", // id is blank + "revocation", + "0", + "statusListCredential", + "BitstringStatusListEntry")) + .isEmpty()); + + assertFalse( + validator + .validate( + new CredentialStatusDto( + "id", + "revocation", + "0", + "", // statusListCredential is blank + "BitstringStatusListEntry")) + .isEmpty()); + } + + @Test + @DisplayName("statusPurpose is invalid") + void invalidStatusPurpose_ThrowsIllegalArgumentException() { + String invalidPurpose = "invalidPurpose"; + + assertThrows( + IllegalArgumentException.class, + () -> { + new CredentialStatusDto( + "id", invalidPurpose, "0", "statusListCredential", "BitstringStatusListEntry"); + }); + } + + @Test + @DisplayName("type is invalid") + void invalidType_ThrowsIllegalArgumentException() { + String invalidType = "invalidType"; + + assertThrows( + IllegalArgumentException.class, + () -> { + new CredentialStatusDto("id", "revocation", "0", "statusListCredential", invalidType); + }); + } + + @Test + @DisplayName("statusPurpose is valid") + void validStatusPurpose_DoesNotThrowException() { + String validPurpose = "revocation"; + + assertDoesNotThrow( + () -> { + new CredentialStatusDto( + "id", validPurpose, "0", "statusListCredential", "BitstringStatusListEntry"); + }); + } + + @Test + @DisplayName("type is valid") + void validType_DoesNotThrowException() { + String validType = "BitstringStatusListEntry"; + + assertDoesNotThrow( + () -> { + new CredentialStatusDto("id", "revocation", "0", "statusListCredential", validType); + }); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java new file mode 100644 index 00000000..5e95b108 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java @@ -0,0 +1,88 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class StatusEntryDtoTest { + + @Test + void validStatusEntryDto_CreatesSuccessfully() { + // Arrange + String validPurpose = PurposeType.REVOCATION.toString(); + + // Act + StatusEntryDto dto = new StatusEntryDto(validPurpose, "issuerId"); + + // Assert + assertNotNull(dto); + assertEquals(validPurpose, dto.purpose()); + assertEquals("issuerId", dto.issuerId()); + } + + @Test + void purposeIsInvalid_ThrowsIllegalArgumentException() { + // Arrange + String invalidPurpose = "invalidPurpose"; + + // Act & Assert + assertThrows( + IllegalArgumentException.class, + () -> { + new StatusEntryDto(invalidPurpose, "issuerId"); + }); + } + + @Test + void anyParameterIsBlank_ThrowsValidationException() { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + // Act & Assert for each field that should not be blank or null + assertThrows( + IllegalArgumentException.class, + () -> + new StatusEntryDto( + "", // purpose is blank + "issuerId")); + assertThrows( + IllegalArgumentException.class, + () -> + new StatusEntryDto( + "suspension", // purpose is blank + "issuerId")); + assertFalse( + validator + .validate( + new StatusEntryDto( + PurposeType.REVOCATION.toString(), "" // issuerId is blank + )) + .isEmpty()); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java new file mode 100644 index 00000000..d1fa870c --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java @@ -0,0 +1,144 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class StatusListCredentialSubjectTest { + + @Test + void builderCreatesObjectWithCorrectValues() { + // Arrange + String id = "12345"; + String statusPurpose = "SomeStatusPurpose"; + String encodedList = "EncodedListData"; + // Act + StatusListCredentialSubject subject = + StatusListCredentialSubject.builder() + .id(id) + .type(StatusListCredentialSubject.TYPE_ENTRY) + .statusPurpose(statusPurpose) + .encodedList(encodedList) + .build(); + // Assert + assertNotNull(subject); + assertEquals(id, subject.getId()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, subject.getType()); + assertEquals(statusPurpose, subject.getStatusPurpose()); + assertEquals(encodedList, subject.getEncodedList()); + } + + @Test + void defaultConstantsAreCorrect() { + // Assert + assertEquals("BitstringStatusListEntry", StatusListCredentialSubject.TYPE_ENTRY); + assertEquals("BitstringStatusList", StatusListCredentialSubject.TYPE_LIST); + assertEquals("id", StatusListCredentialSubject.SUBJECT_ID); + assertEquals("type", StatusListCredentialSubject.SUBJECT_TYPE); + assertEquals("statusPurpose", StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE); + assertEquals("encodedList", StatusListCredentialSubject.SUBJECT_ENCODED_LIST); + } + + @Test + void builderCreatesCredentialTypeObject() { + // Arrange + String id = "67890"; + String statusPurpose = "AnotherStatusPurpose"; + String encodedList = "AnotherEncodedListData"; + // Act + StatusListCredentialSubject subject = + StatusListCredentialSubject.builder() + .id(id) + .type(StatusListCredentialSubject.TYPE_LIST) + .statusPurpose(statusPurpose) + .encodedList(encodedList) + .build(); + // Assert + assertNotNull(subject); + assertEquals(id, subject.getId()); + assertEquals(StatusListCredentialSubject.TYPE_LIST, subject.getType()); + assertEquals(statusPurpose, subject.getStatusPurpose()); + assertEquals(encodedList, subject.getEncodedList()); + } + + @Test + void builderWithNullValuesForOptionalFields() { + // Arrange and Act + StatusListCredentialSubject subject = + StatusListCredentialSubject.builder() + .id("idWithNulls") + .type(StatusListCredentialSubject.TYPE_ENTRY) + // Optional fields are not set (statusPurpose, encodedList) + .build(); + // Assert + assertNotNull(subject); + assertEquals("idWithNulls", subject.getId()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, subject.getType()); + assertNull(subject.getStatusPurpose()); // Checks if the field is truly optional and nullable + assertNull(subject.getEncodedList()); + } + + @Test + void objectsAreImmutable() { + // Arrange + StatusListCredentialSubject subject = + StatusListCredentialSubject.builder() + .id("immutableId") + .type(StatusListCredentialSubject.TYPE_ENTRY) + .statusPurpose("PurposeBeforeChange") + .encodedList("ListBeforeChange") + .build(); + // Act + // Attempt to change the properties after creation to check if the object is immutable + // This attempt should not change the object since the class is expected to be immutable + // Since the class doesn't provide setters, this test ensures that the builder pattern itself + // does not introduce mutability + // Assert + assertEquals("immutableId", subject.getId()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, subject.getType()); + assertEquals("PurposeBeforeChange", subject.getStatusPurpose()); + assertEquals("ListBeforeChange", subject.getEncodedList()); + } + + @Test + void testToString() { + // Arrange + String id = "12345"; + String statusPurpose = "SomeStatusPurpose"; + String encodedList = "EncodedListData"; + // Act + String s = + StatusListCredentialSubject.builder() + .id(id) + .type(StatusListCredentialSubject.TYPE_ENTRY) + .statusPurpose(statusPurpose) + .encodedList(encodedList) + .toString(); + assertNotNull(s); + assertFalse(s.isEmpty()); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java new file mode 100644 index 00000000..6b040616 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java @@ -0,0 +1,70 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class TokenResponeTest { + @Test + void getAndSetAccessToken() { + // Arrange + TokenResponse tokenResponse = new TokenResponse(); + String expectedToken = "someAccessToken123"; + + // Act + tokenResponse.setAccessToken(expectedToken); + + // Assert + String actualToken = tokenResponse.getAccessToken(); + assertEquals(expectedToken, actualToken, "someAccessToken123"); + } + + @Test + void setAccessTokenWithNullValue() { + // Arrange + TokenResponse tokenResponse = new TokenResponse(); + + // Act + tokenResponse.setAccessToken(null); + + // Assert + String actualToken = tokenResponse.getAccessToken(); + assertNull(actualToken); + } + + @Test + void setAccessTokenWithEmptyString() { + // Arrange + TokenResponse tokenResponse = new TokenResponse(); + String expectedToken = ""; + + // Act + tokenResponse.setAccessToken(expectedToken); + + // Assert + String actualToken = tokenResponse.getAccessToken(); + assertEquals(expectedToken, actualToken, ""); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerExceptionTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerExceptionTest.java new file mode 100644 index 00000000..87e0dc2e --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/BitSetManagerExceptionTest.java @@ -0,0 +1,39 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class BitSetManagerExceptionTest { + + @Test + void testConstructors() { + assertDoesNotThrow(() -> new BitSetManagerException()); + assertDoesNotThrow(() -> new BitSetManagerException("hallo")); + assertDoesNotThrow(() -> new BitSetManagerException("hallo", new IllegalArgumentException())); + assertDoesNotThrow( + () -> new BitSetManagerException("hallo", new IllegalArgumentException(), false, false)); + assertDoesNotThrow(() -> new BitSetManagerException(new IllegalArgumentException())); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceExceptionTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceExceptionTest.java new file mode 100644 index 00000000..82be0a15 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/RevocationServiceExceptionTest.java @@ -0,0 +1,36 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.exception; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class RevocationServiceExceptionTest { + @Test + void testConstructors() { + assertDoesNotThrow(() -> new RevocationServiceException("hallo")); + assertDoesNotThrow( + () -> new RevocationServiceException("hallo", new IllegalArgumentException())); + assertDoesNotThrow(() -> new RevocationServiceException(new IllegalArgumentException())); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredentialTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredentialTest.java new file mode 100644 index 00000000..53f30f4d --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListCredentialTest.java @@ -0,0 +1,159 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.jpa; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BPN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DataJpaTest +@AutoConfigureJson +class StatusListCredentialTest { + + @MockBean + ObjectMapper objectMapper; + + @Autowired + private TestEntityManager entityManager; + + private LocalValidatorFactoryBean validator; + + @BeforeEach + public void setUp() { + validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); // Initializes the validator + } + + private VerifiableCredential createVerifiableCredentialTestData(String subjectId) { + Map credentialData = new HashMap<>(); + credentialData.put("id", "urn:uuid:" + UUID.randomUUID()); + credentialData.put("issuer", "https://issuer.example.com"); + credentialData.put("issuanceDate", Instant.now().toString()); + credentialData.put("type", List.of("VerifiableCredential", "StatusListCredential")); + + Map subjectData = new HashMap<>(); + subjectData.put("id", subjectId); + subjectData.put("type", "StatusList2021Credential"); + credentialData.put("credentialSubject", subjectData); + credentialData.put("@context", VerifiableCredential.DEFAULT_CONTEXT.toString()); + + return new VerifiableCredential(credentialData); + } + + @Test + void testStatusListCredentialPersistence() { + // Arrange + VerifiableCredential credential = + createVerifiableCredentialTestData("urn:uuid:" + UUID.randomUUID()); + + StatusListCredential statusListCredential = + StatusListCredential.builder() + .id(BPN + "revocation#1") + .issuerBpn(BPN) + .credential(credential) + .build(); + + // Act + StatusListCredential persistedStatusListCredential = + entityManager.persistFlushFind(statusListCredential); + + // Assert + assertNotNull(persistedStatusListCredential); + assertEquals(BPN + "revocation#1", persistedStatusListCredential.getId()); + assertEquals(BPN, persistedStatusListCredential.getIssuerBpn()); + assertEquals(credential, persistedStatusListCredential.getCredential()); + } + + @Test + void givenInvalidId_whenSaving_thenValidationFails() { + // Arrange + StatusListCredential statusListCredential = + StatusListCredential.builder() + .issuerBpn("") // Invalid issuerId + .credential(createVerifiableCredentialTestData("urn:uuid:" + UUID.randomUUID())) + .build(); + + // Act and Assert + Set> violations = + validator.validate(statusListCredential); + assertThat(violations).isNotEmpty(); + assertThat(violations.toString()).contains("ID cannot be blank"); + } + + @Test + void givenNullCredential_whenSaving_thenThrowsException() { + // Arrange + StatusListCredential statusListCredential = + StatusListCredential.builder() + .id(BPN + "revocation#1") + .issuerBpn(BPN) + .credential(null) // Null Credential + .build(); + + // Act and Assert + Exception exception = + assertThrows( + ConstraintViolationException.class, + () -> { + entityManager.persistFlushFind(statusListCredential); + }); + String expectedMessage = "Credential cannot be null"; + assertThat(exception.getMessage()).contains(expectedMessage); + } + + @Test + void givenInvalidCredentialId_whenSaving_thenValidationFails() { + // Arrange + VerifiableCredential invalidCredential = + createVerifiableCredentialTestData(UUID.randomUUID().toString()); + + StatusListCredential statusListCredential = + StatusListCredential.builder().issuerBpn(BPN).credential(invalidCredential).build(); + + // Act and Assert + Set> violations = + validator.validate(statusListCredential); + assertThat(violations).isNotEmpty(); // Because the `credential` field validation would fail + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java new file mode 100644 index 00000000..e163cce0 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java @@ -0,0 +1,165 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.jpa; + +import jakarta.validation.ConstraintViolation; +import org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BPN; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(SpringExtension.class) +@AutoConfigureJson +@DataJpaTest +public class StatusListIndexTest { + + @Autowired + private TestEntityManager entityManager; + + private LocalValidatorFactoryBean validator; + + @BeforeEach + public void setUp() { + validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); // Initializes the validator + } + + @Test + public void whenFieldsAreValid_thenNoConstraintViolationsAndEntityPersists() { + String id = BPN + "-revocation#1"; + String issuerBpnStatus = BPN + "-revocation"; + + StatusListIndex statusListIndex = + StatusListIndex.builder() + .id(id) + .issuerBpnStatus(issuerBpnStatus) + .currentIndex("123456") // using numeric string only as index + .build(); + + Set> violations = validator.validate(statusListIndex); + assertThat(violations).isEmpty(); + + entityManager.persistAndFlush(statusListIndex); + + StatusListIndex found = entityManager.find(StatusListIndex.class, statusListIndex.getId()); + assertThat(found).isNotNull(); + assertThat(found.getIssuerBpnStatus()).isEqualTo(statusListIndex.getIssuerBpnStatus()); + assertThat(found.getCurrentIndex()).isEqualTo(statusListIndex.getCurrentIndex()); + } + + @Test + public void whenIdIsBlank_thenConstraintViolationOccurs() { + StatusListIndex statusListIndex = + StatusListIndex.builder().id(" ").currentIndex("123456").build(); + Set> violations = validator.validate(statusListIndex); + + assertThat(violations).isNotEmpty(); + assertThat(violations.toString()).contains("ID cannot be blank"); + } + + @Test + public void whenIssuerBpnStatusIsBlank_thenConstraintViolationOccurs() { + StatusListIndex statusListIndex = + StatusListIndex.builder() + .issuerBpnStatus(" ") + .id(BPN + "-revocation#1") + .currentIndex("123456") + .build(); + Set> violations = validator.validate(statusListIndex); + + assertThat(violations).isNotEmpty(); + assertThat(violations.toString()).contains("Issuer BPN with status cannot be blank"); + } + + @Test + public void whenCurrentIndexIsBlank_thenConstraintViolationOccurs() { + StatusListIndex statusListIndex = + StatusListIndex.builder().issuerBpnStatus(TestUtil.BPN).currentIndex(" ").build(); + Set> violations = validator.validate(statusListIndex); + + assertThat(violations).isNotEmpty(); + assertThat(violations.toString()).contains("Current index cannot be blank"); + } + + @Test + public void whenCurrentIndexIsNotNumeric_thenConstraintViolationOccurs() { + String id = BPN + "-revocation#1"; + String issuerBpnStatus = BPN + "-revocation"; + String wrongIndex = "indexABC"; + + StatusListIndex statusListIndex = + StatusListIndex.builder() + .id(id) + .issuerBpnStatus(issuerBpnStatus) + .currentIndex(wrongIndex) // invalid non-numeric currentIndex + .build(); + Set> violations = validator.validate(statusListIndex); + + assertThat(violations).isNotEmpty(); + assertThat(violations.toString()).contains("Current index must be numeric"); + } + + @Test + public void whenSetInvalidCurrentIndex_thenIllegalArgumentExceptionIsThrown() { + // Constructing StatusListIndex using the builder pattern + // with a valid issuerId and leaving currentIndex unset initially + StatusListIndex statusListIndex = + StatusListIndex.builder().issuerBpnStatus(BPN + "-revocation").build(); + + // Now we attempt to set an invalid (non-numeric) currentIndex + // using the setCurrentIndex method which includes validation + assertThrows(IllegalArgumentException.class, () -> statusListIndex.setCurrentIndex("indexABC")); + } + + @Test + public void whenFieldsExceedSizeLimit_thenConstraintViolationOccurs() { + String longIssuerBpnStatus = BPN + "-revocation1"; + String longCurrentIndex = + "12345".repeat(4); // The repeat count adjusts on the max size of Index + String id = "normalid".repeat(76); // The repeat count adjusts on the max length of ID + StatusListIndex statusListIndex = + StatusListIndex.builder() + .issuerBpnStatus(longIssuerBpnStatus) + .currentIndex(longCurrentIndex) + .id(id) + .build(); + + Set> violations = validator.validate(statusListIndex); + + assertThat(violations).isNotEmpty(); + + assertThat(violations.toString()).contains("ID cannot exceed 256 characters"); + assertThat(violations.toString()).contains("Current index cannot exceed 16 characters"); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java new file mode 100644 index 00000000..6f530935 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java @@ -0,0 +1,159 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.services; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import org.eclipse.tractusx.managedidentitywallets.revocation.config.security.SecurityConfigProperties; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.TokenResponse; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClient; + +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.jsonResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockEmptyEncodedList; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockStatusListCredential; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockStatusListVC; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +class HttpClientServiceTest { + + @RegisterExtension + static WireMockExtension wm1 = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + private static SecurityConfigProperties securityConfigProperties; + + private static HttpClientService httpClientService; // The service to test + + @Mock + private RestClient.Builder webClientBuilder; // Assuming RestClient is using WebClient.Builder + + @Mock + private RestClient webClient; + + @Mock + private RestClient.RequestBodyUriSpec requestBodyUriSpec; + + @Mock + private RestClient.RequestBodySpec requestBodySpec; + + @Mock + private RestClient.RequestHeadersSpec requestHeadersSpec; + + @Mock + private RestClient.ResponseSpec responseSpec; + + @BeforeAll + public static void beforeAll() { + securityConfigProperties = Mockito.mock(SecurityConfigProperties.class); + when(securityConfigProperties.publicClientId()).thenReturn("public-client-id"); + when(securityConfigProperties.clientId()).thenReturn("client-id"); + when(securityConfigProperties.tokenUrl()).thenReturn(wm1.baseUrl() + "/token"); + httpClientService = new HttpClientService(securityConfigProperties); + ReflectionTestUtils.setField(httpClientService, "miwUrl", wm1.baseUrl()); + } + + @BeforeEach + void setUp() { + } + + @Test + void testGetBearerToken_Success() { + String expectedToken = "mockToken"; + TokenResponse mockTokenResponse = new TokenResponse(); + mockTokenResponse.setAccessToken(expectedToken); + wm1.stubFor(post("/token").willReturn(jsonResponse(mockTokenResponse, 200))); + String token = httpClientService.getBearerToken(); + assertEquals(expectedToken, token); + } + + // 4XX HttpClientErrorException + // 5XX HttpServerErrorException + @ParameterizedTest + @ValueSource(ints = { 500, 400 }) + void testGetBearerToken_Error(int code) { + String expectedToken = "mockToken"; + TokenResponse mockTokenResponse = new TokenResponse(); + mockTokenResponse.setAccessToken(expectedToken); + wm1.stubFor(post("/token").willReturn(jsonResponse(mockTokenResponse, code))); + if (code == 400) + assertThrows(HttpClientErrorException.class, () -> httpClientService.getBearerToken()); + else assertThrows(HttpServerErrorException.class, () -> httpClientService.getBearerToken()); + } + + @Test + void testSignStatusListVC_Success() { + final var issuer = "did:web:localhost:BPNL345345345345"; + var fragment = UUID.randomUUID().toString(); + var encodedList = mockEmptyEncodedList(); + var credentialBuilder = mockStatusListVC(issuer, fragment, encodedList); + var unsignedCredential = credentialBuilder.build(); + var statusListCredential = mockStatusListCredential(issuer, credentialBuilder); + TokenResponse tokenResponse = new TokenResponse(); + tokenResponse.setAccessToken("123456"); + wm1.stubFor(post("/token").willReturn(jsonResponse(tokenResponse, 200))); + wm1.stubFor( + post("/api/credentials?isRevocable=false") + .willReturn(jsonResponse(statusListCredential.getCredential(), 200))); + VerifiableCredential signedCredential = + assertDoesNotThrow( + () -> + httpClientService.signStatusListVC( + unsignedCredential, httpClientService.getBearerToken())); + assertThat(signedCredential).hasFieldOrProperty("proof"); + } + + @Test + void testSignStatusListVC_Error() { + wm1.stubFor(post("/api/credentials").willReturn(aResponse().withStatus(400))); + + final var issuer = "did:web:localhost:BPNL345345345345"; + var fragment = UUID.randomUUID().toString(); + var encodedList = mockEmptyEncodedList(); + var credentialBuilder = mockStatusListVC(issuer, fragment, encodedList); + var unsignedCredential = credentialBuilder.build(); + + // HttpClientErrorException extends RestClientException + assertThrows( + HttpClientErrorException.class, + () -> httpClientService.signStatusListVC(unsignedCredential, "dummy")); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java new file mode 100644 index 00000000..3d98c148 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java @@ -0,0 +1,410 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil; +import org.eclipse.tractusx.managedidentitywallets.revocation.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusListCredentialSubject; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.BitSetManagerException; +import org.eclipse.tractusx.managedidentitywallets.revocation.exception.RevocationServiceException; +import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListIndex; +import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListCredentialRepository; +import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListIndexRepository; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; + +import java.net.URI; +import java.util.Base64; +import java.util.BitSet; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BITSET_SIZE; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BPN; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.DID; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.VC_CONTEXTS; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.decompressGzip; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockEmptyEncodedList; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockStatusListCredential; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockStatusListIndex; +import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.mockStatusListVC; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +class RevocationServiceTest { + + private static final String CALLER_BPN = UUID.randomUUID().toString(); + + private static StatusListCredentialRepository statusListCredentialRepository; + + private static StatusListIndexRepository statusListIndexRepository; + + private static HttpClientService httpClientService; + + private static RevocationService revocationService; + + private static MIWSettings miwSettings; + + @BeforeAll + public static void beforeAll() { + statusListCredentialRepository = Mockito.mock(StatusListCredentialRepository.class); + statusListIndexRepository = Mockito.mock(StatusListIndexRepository.class); + httpClientService = Mockito.mock(HttpClientService.class); + miwSettings = new MIWSettings(VC_CONTEXTS); + httpClientService.domainUrl = "http://example.com"; + revocationService = + new RevocationService( + statusListCredentialRepository, + statusListIndexRepository, + httpClientService, + miwSettings); + } + + @BeforeEach + public void beforeEach() { + Mockito.reset(statusListCredentialRepository, statusListIndexRepository, httpClientService); + } + + @Nested + class RevokeTest { + + @Test + void shouldRevokeCredential() { + final var issuer = DID; + var encodedList = mockEmptyEncodedList(); + var credentialBuilder = mockStatusListVC(issuer, "1", encodedList); + var statusListCredential = mockStatusListCredential(issuer, credentialBuilder); + // 1. create status list with the credential + var statusListIndex = mockStatusListIndex(issuer, statusListCredential, "0"); + when(statusListIndex.getStatusListCredential()).thenReturn(statusListCredential); + when(statusListCredentialRepository.findById(any(String.class))) + .thenReturn(Optional.of(statusListCredential)); + CredentialStatusDto credentialStatusDto = Mockito.mock(CredentialStatusDto.class); + when(credentialStatusDto.id()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1#0"); + when(credentialStatusDto.statusPurpose()).thenReturn("revocation"); + when(credentialStatusDto.statusListIndex()).thenReturn("0"); + when(credentialStatusDto.statusListCredential()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1"); + when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + assertDoesNotThrow(() -> revocationService.revoke(credentialStatusDto, "token")); + Mockito.verify(statusListCredentialRepository, Mockito.times(1)) + .saveAndFlush(eq(statusListCredential)); + ArgumentCaptor captor = + ArgumentCaptor.forClass(VerifiableCredential.class); + Mockito.verify(httpClientService) + .signStatusListVC(captor.capture(), Mockito.any(String.class)); + VerifiableCredential newList = captor.getValue(); + VerifiableCredentialSubject verifiableCredentialSubject = + newList.getCredentialSubject().get(0); + String encodedNewList = (String) verifiableCredentialSubject.get("encodedList"); + byte[] decodedNewList = Base64.getDecoder().decode(encodedNewList); + BitSet decompressedNewList = decompressGzip(decodedNewList); + byte[] decodedList = Base64.getDecoder().decode(encodedList); + BitSet decompressedList = decompressGzip(decodedList); + assertFalse(decompressedList.get(0)); + assertTrue(decompressedNewList.get(0)); + } + + @Test + void shouldThrowRevocationServiceException() { + final var issuer = DID; + var encodedList = mockEmptyEncodedList(); + var credentialBuilder = mockStatusListVC(issuer, "1", encodedList); + var statusListCredential = mockStatusListCredential(issuer, credentialBuilder); + // 1. create status list with the credential + var statusListIndex = mockStatusListIndex(issuer, statusListCredential, "0"); + when(statusListIndex.getStatusListCredential()).thenReturn(statusListCredential); + when(statusListCredentialRepository.findById(any(String.class))) + .thenReturn(Optional.of(statusListCredential)); + CredentialStatusDto credentialStatusDto = Mockito.mock(CredentialStatusDto.class); + when(credentialStatusDto.id()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1#0"); + when(credentialStatusDto.statusPurpose()).thenReturn("revocation"); + when(credentialStatusDto.statusListIndex()).thenReturn("0"); + when(credentialStatusDto.statusListCredential()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1"); + when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + try (MockedStatic utilities = Mockito.mockStatic(BitSetManager.class)) { + utilities + .when(() -> BitSetManager.revokeCredential(any(String.class), any(Integer.class))) + .thenThrow(new BitSetManagerException()); + assertThrows( + RevocationServiceException.class, + () -> revocationService.revoke(credentialStatusDto, "token")); + } + } + } + + @Nested + class CreateStatusListTest { + + @Test + void shouldCreateNewStatusList() { + ReflectionTestUtils.setField(httpClientService, "domainUrl", "http://this-is-my-domain"); + StatusEntryDto mockStatus = Mockito.mock(StatusEntryDto.class); + when(mockStatus.issuerId()).thenReturn(DID); + when(mockStatus.purpose()).thenReturn("revocation"); + CredentialStatusDto credentialStatusDto = + assertDoesNotThrow(() -> revocationService.createStatusList(mockStatus, "token")); + assertEquals("revocation", credentialStatusDto.statusPurpose()); + assertEquals( + "http://this-is-my-domain/api/v1/revocations/credentials/" + BPN + "/revocation/1#0", + credentialStatusDto.id()); + assertEquals("0", credentialStatusDto.statusListIndex()); + assertEquals( + "http://this-is-my-domain/api/v1/revocations/credentials/" + BPN + "/revocation/1", + credentialStatusDto.statusListCredential()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, credentialStatusDto.type()); + Mockito.verify(statusListIndexRepository, times(1)).save(any(StatusListIndex.class)); + } + + @Test + void shouldUpdateExistingStatusList() { + ReflectionTestUtils.setField(httpClientService, "domainUrl", "http://this-is-my-domain"); + StatusListIndex statusListIndex = + StatusListIndex.builder() + .currentIndex("0") + .id(BPN + "-revocation#1") + .issuerBpnStatus(BPN + "-revocation") + .build(); + when(statusListIndexRepository.findByIssuerBpnStatus(BPN + "-revocation")) + .thenReturn(List.of(statusListIndex)); + StatusEntryDto mockStatus = Mockito.mock(StatusEntryDto.class); + when(mockStatus.issuerId()).thenReturn(DID); + when(mockStatus.purpose()).thenReturn("revocation"); + CredentialStatusDto credentialStatusDto = + assertDoesNotThrow(() -> revocationService.createStatusList(mockStatus, "token")); + assertEquals("revocation", credentialStatusDto.statusPurpose()); + assertEquals( + "http://this-is-my-domain/api/v1/revocations/credentials/" + BPN + "/revocation/1#1", + credentialStatusDto.id()); + assertEquals("1", credentialStatusDto.statusListIndex()); + assertEquals( + "http://this-is-my-domain/api/v1/revocations/credentials/" + BPN + "/revocation/1", + credentialStatusDto.statusListCredential()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, credentialStatusDto.type()); + Mockito.verify(statusListIndexRepository, times(1)).save(any(StatusListIndex.class)); + } + + @Test + void shouldCreateNewStatusListWhenFirstFull() { + ReflectionTestUtils.setField(httpClientService, "domainUrl", "http://this-is-my-domain"); + StatusListIndex statusListIndex = + StatusListIndex.builder() + .currentIndex(String.valueOf(BITSET_SIZE - 1)) + .id(BPN + "-revocation#1") + .issuerBpnStatus(BPN + "-revocation") + .build(); + when(statusListIndexRepository.findByIssuerBpnStatus(BPN + "-revocation")) + .thenReturn(List.of(statusListIndex)); + StatusEntryDto mockStatus = Mockito.mock(StatusEntryDto.class); + when(mockStatus.issuerId()).thenReturn(DID); + when(mockStatus.purpose()).thenReturn("revocation"); + CredentialStatusDto credentialStatusDto = + assertDoesNotThrow(() -> revocationService.createStatusList(mockStatus, "token")); + assertEquals("revocation", credentialStatusDto.statusPurpose()); + assertEquals( + "http://this-is-my-domain/api/v1/revocations/credentials/" + BPN + "/revocation/2#0", + credentialStatusDto.id()); + assertEquals("0", credentialStatusDto.statusListIndex()); + assertEquals( + "http://this-is-my-domain/api/v1/revocations/credentials/" + BPN + "/revocation/2", + credentialStatusDto.statusListCredential()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, credentialStatusDto.type()); + Mockito.verify(statusListIndexRepository, times(1)).save(any(StatusListIndex.class)); + } + } + + @Nested + class GetStatusListCredential { + + @Test + void shouldGetList() throws JsonProcessingException { + final var issuer = DID; + var fragment = UUID.randomUUID().toString(); + var encodedList = mockEmptyEncodedList(); + var credentialBuilder = mockStatusListVC(issuer, fragment, encodedList); + var statusListCredential = mockStatusListCredential(issuer, credentialBuilder); + when(statusListCredentialRepository.findById(any(String.class))) + .thenReturn(Optional.of(statusListCredential)); + VerifiableCredential verifiableCredential = + assertDoesNotThrow( + () -> revocationService.getStatusListCredential(BPN, "revocation", "1")); + assertNotNull(verifiableCredential); + assertEquals( + URI.create(TestUtil.STATUS_LIST_CREDENTIAL_SUBJECT_ID), + verifiableCredential.getCredentialSubject().get(0).getId()); + assertEquals(URI.create(issuer), verifiableCredential.getIssuer()); + assertEquals(URI.create(issuer + "#" + fragment), verifiableCredential.getId()); + assertEquals(verifiableCredential.getContext(), miwSettings.vcContexts()); + } + + @Test + void shouldReturnNull() { + when(statusListCredentialRepository.findById(any(String.class))).thenReturn(Optional.empty()); + VerifiableCredential verifiableCredential = + assertDoesNotThrow(() -> revocationService.getStatusListCredential("", "", "")); + assertNull(verifiableCredential); + } + } + + @Nested + class CheckSubStringExtraction { + @Test + void shouldExtractBpnFromDid() { + assertEquals(revocationService.extractBpnFromDid(DID), BPN); + } + + @Test + void shouldExtractIdFromURL() { + assertEquals( + revocationService.extractIdFromURL( + "http://this-is-my-domain/api/v1/revocations/credentials/BPNL123456789000/revocation/1"), + "BPNL123456789000-revocation#1"); + } + + @Test + void shouldExtractIdFromURLCaseSensitive() { + assertEquals( + revocationService.extractIdFromURL( + "http://this-is-my-domain/api/v1/revocations/credentials/bpnl123456789000/revocation/1"), + "BPNL123456789000-revocation#1"); + } + + @Test + void shouldExtractBpnFromURL() { + assertEquals( + revocationService.extractBpnFromURL( + "http://this-is-my-domain/api/v1/revocations/credentials/BPNL123456789000/revocation/1"), + BPN); + } + + @Test + void shouldExtractBpnFromURLCaseSensitive() { + assertEquals( + revocationService.extractBpnFromURL( + "http://this-is-my-domain/api/v1/revocations/creDENTials/bpNl123456789000/revocation/1"), + BPN); + } + } + + @Nested + class ValidateCredentialStatus { + + @Test + @DisplayName("statusPurpose is valid") + void validCredentialStatusDto() { + String statusIndex = "1"; + String statusListCredential = + "http://example.com/api/v1/revocations/credentials/" + BPN + "/revocation/1"; + String id = statusListCredential + "#" + statusIndex; + + CredentialStatusDto dto = + new CredentialStatusDto( + id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + + assertDoesNotThrow(() -> revocationService.validateCredentialStatus(dto)); + } + + @Test + @DisplayName("statusPurpose from dto is not matching the credential status list url") + void invalidStatusPurpose_ThrowsIllegalArgumentException() { + String statusIndex = "1"; + String invalidPurpose = "/break/"; + String statusListCredential = + "http://example.com/api/v1/revocations/credentials/" + BPN + invalidPurpose + "1"; + String id = statusListCredential + "#" + statusIndex; + + CredentialStatusDto dto = + new CredentialStatusDto( + id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + assertThrows( + IllegalArgumentException.class, () -> revocationService.validateCredentialStatus(dto)); + } + + @Test + @DisplayName("id url from dto is not matching the credential status list url") + void invalidId_ThrowsIllegalArgumentException() { + String statusIndex = "1"; + String statusListCredential = + "http://example.com/api/v1/revocations/credentials/" + BPN + "/revocation/1"; + String id = statusListCredential.replace(BPN, "BPN0101010101010") + "#2"; + + CredentialStatusDto dto = + new CredentialStatusDto( + id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + assertThrows( + IllegalArgumentException.class, () -> revocationService.validateCredentialStatus(dto)); + } + + @Test + @DisplayName("credential status index is not matching the index in the url") + void invalidStatusIndex_ThrowsIllegalArgumentException() { + String statusIndex = "1"; + String statusListCredential = + "http://example.com/api/v1/revocations/credentials/" + BPN + "/revocation/1"; + String id = statusListCredential + "#2"; + + CredentialStatusDto dto = + new CredentialStatusDto( + id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + assertThrows( + IllegalArgumentException.class, () -> revocationService.validateCredentialStatus(dto)); + } + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java new file mode 100644 index 00000000..6e41da99 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java @@ -0,0 +1,109 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.utils; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.BitSet; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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; + +class BitSetManagerTest { + + @Test + void initializeEncodedListString_ReturnsValidBase64() { + String encoded = BitSetManager.initializeEncodedListString(); + assertNotNull(encoded); + // Attempt to decode to verify it's valid Base64. + assertDoesNotThrow(() -> BitSetManager.decodeFromString(encoded)); + } + + @Test + void compress_And_Decompress_ReturnsOriginalBitSet() { + BitSet originalBitSet = new BitSet(BitSetManager.BITSET_SIZE); + originalBitSet.set(100); + originalBitSet.set(10000); + + byte[] compressedBytes = BitSetManager.compress(originalBitSet); + assertNotNull(compressedBytes); + + BitSet decompressedBitSet = BitSetManager.decompress(compressedBytes); + assertNotNull(decompressedBitSet); + assertEquals(originalBitSet, decompressedBitSet); + } + + @Test + void revokeCredential_SetsBitAndReturnsUpdatedEncodedList() throws Exception { + String encodedList = BitSetManager.initializeEncodedListString(); + int indexToRevoke = 99; + + String updatedEncodedList = BitSetManager.revokeCredential(encodedList, indexToRevoke); + assertNotNull(updatedEncodedList); + + BitSet updatedBitSet = + BitSetManager.decompress(BitSetManager.decodeFromString(updatedEncodedList)); + assertTrue(updatedBitSet.get(indexToRevoke)); + } + + @Test + @Disabled("should this even be checked here?") + void revokeCredential_AlreadyRevoked_ThrowsException() { + String encodedList = BitSetManager.initializeEncodedListString(); + int indexToRevoke = 99; + + // First time revoking should be fine + assertDoesNotThrow(() -> BitSetManager.revokeCredential(encodedList, indexToRevoke)); + + // Second time revoking should throw Exception + assertThrows(Exception.class, () -> BitSetManager.revokeCredential(encodedList, indexToRevoke)); + } + + @Test + void suspendCredential_FlipsBitAndReturnsUpdatedEncodedList() { + String encodedList = BitSetManager.initializeEncodedListString(); + int indexToSuspend = 99; + + String updatedEncodedList = BitSetManager.suspendCredential(encodedList, indexToSuspend); + assertNotNull(updatedEncodedList); + + BitSet updatedBitSet = + BitSetManager.decompress(BitSetManager.decodeFromString(updatedEncodedList)); + assertTrue(updatedBitSet.get(indexToSuspend)); // Suspend should flip the bit to true + } + + @Test + void encodeToStringAndDecodeFromString_AreReversible() { + byte[] originalData = new byte[]{ 1, 2, 3, 4, 5 }; + String encoded = BitSetManager.encodeToString(originalData); + assertNotNull(encoded); + + byte[] decodedData = BitSetManager.decodeFromString(encoded); + assertNotNull(decodedData); + assertTrue(Arrays.equals(originalData, decodedData)); + } +} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java new file mode 100644 index 00000000..159e1fd6 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java @@ -0,0 +1,49 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ValidateTest { + + @Test + void validateTest() { + Assertions.assertThrows( + RuntimeException.class, () -> Validate.isFalse(false).launch(new RuntimeException())); + Assertions.assertThrows( + RuntimeException.class, () -> Validate.isTrue(true).launch(new RuntimeException())); + Assertions.assertThrows( + RuntimeException.class, () -> Validate.isNull(null).launch(new RuntimeException())); + Assertions.assertThrows( + RuntimeException.class, () -> Validate.isNotNull("Test").launch(new RuntimeException())); + Assertions.assertThrows( + RuntimeException.class, + () -> Validate.value("").isNotEmpty().launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isFalse(true).launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isTrue(false).launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isNull("").launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isNotNull(null).launch(new RuntimeException())); + Assertions.assertDoesNotThrow( + () -> Validate.value("Test").isNotEmpty().launch(new RuntimeException())); + } +} diff --git a/settings.gradle b/settings.gradle index b2cb866c..f93bc271 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,3 +4,4 @@ rootProject.name = 'managedidentitywallets' // for example: // include '' include 'miw' +include 'revocation-service' From 042179741fced9a6119ca29568ff88cf54b93b49 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 13 Jun 2024 15:25:59 +0530 Subject: [PATCH 16/60] doc: API doc added for revocation --- docs/api/revocation-service/openapi_v001.json | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 docs/api/revocation-service/openapi_v001.json diff --git a/docs/api/revocation-service/openapi_v001.json b/docs/api/revocation-service/openapi_v001.json new file mode 100644 index 00000000..ba06ff8d --- /dev/null +++ b/docs/api/revocation-service/openapi_v001.json @@ -0,0 +1,277 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Reovcation Service API", + "description": "Revocation Service API", + "contact": { + "name": "eclipse-tractusx", + "url": "https://projects.eclipse.org/projects/automotive.tractusx", + "email": "tractusx-dev@eclipse.org" + }, + "version": "0.0.1" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "security": [ + { + "Authenticate using access_token": [] + } + ], + "tags": [ + { + "name": "Revocation Service", + "description": "Revocation Service API" + } + ], + "paths": { + "/api/v1/revocations/status-entry": { + "post": { + "tags": [ + "Revocation Service" + ], + "summary": "Create or Update a Status List Credential", + "description": "Create the status list credential if it does not exist, else update it.", + "operationId": "createStatusListVC", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatusEntryDto" + }, + "example": { + "purpose": "revocation", + "issuerId": "did:web:localhost:BPNL000000000000" + } + } + }, + "required": true + }, + "responses": { + "403": { + "description": "ForbiddenException: invalid caller" + }, + "200": { + "description": "Status list credential created/updated successfully.", + "content": { + "application/json": { + "example": { + "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusListEntry" + } + } + } + }, + "401": { + "description": "UnauthorizedException: invalid token" + }, + "500": { + "description": "RevocationServiceException: Internal Server Error" + } + } + } + }, + "/api/v1/revocations/revoke": { + "post": { + "tags": [ + "Revocation Service" + ], + "summary": "Revoke a VerifiableCredential", + "description": "Revoke a VerifiableCredential using the provided Credential Status", + "operationId": "revokeCredential", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusDto" + }, + "example": { + "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusListEntry" + } + } + }, + "required": true + }, + "responses": { + "403": { + "description": "ForbiddenException: invalid caller" + }, + "200": { + "description": "Verifiable credential revoked successfully." + }, + "401": { + "description": "UnauthorizedException: invalid token" + }, + "500": { + "description": "RevocationServiceException: Internal Server Error" + }, + "409": { + "description": "ConflictException: Revocation service error", + "content": { + "application/json": { + "example": { + "type": "BitstringStatusListEntry", + "title": "Revocation service error", + "status": 409, + "detail": "Credential already revoked", + "instance": "/api/v1/revocations/revoke", + "timestamp": 1707133388128 + } + } + } + } + } + } + }, + "/api/v1/revocations/credentials/{issuerBPN}/{status}/{index}": { + "get": { + "tags": [ + "Revocation Service" + ], + "summary": "Get status list credential", + "description": "Get status list credential using the provided issuer BPN and status purpose and status list index", + "operationId": "getStatusListCredential", + "parameters": [ + { + "name": "issuerBPN", + "in": "path", + "description": "Issuer BPN", + "required": true, + "schema": { + "type": "string" + }, + "example": "BPNL000000000000" + }, + { + "name": "status", + "in": "path", + "description": "Status Purpose ( Revocation or Suspension)", + "required": true, + "schema": { + "type": "string" + }, + "example": "revocation" + }, + { + "name": "index", + "in": "path", + "description": "status list index", + "required": true, + "schema": { + "type": "string" + }, + "example": 1 + } + ], + "responses": { + "404": { + "description": "Status list credential not found" + }, + "500": { + "description": "RevocationServiceException: Internal Server Error" + }, + "200": { + "description": "Get Status list credential ", + "content": { + "application/json": { + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "issuer": "did:web:localhost:BPNL000000000000", + "issuanceDate": "2024-02-05T09:39:58Z", + "credentialSubject": [ + { + "statusPurpose": "revocation", + "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusList", + "encodedList": "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" + } + ], + "proof": { + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "did:web:localhost:BPNL000000000000#ed463e4c-b900-481a-b5d0-9ae439c434ae", + "created": "2024-02-05T09:39:58Z", + "jws": "eyJhbGciOiJFZERTQSJ9..swX1PLJkSlxB6JMmY4a2uUzR-uszlyLrVdNppoYSx4PTV1LzQrDb0afzp_dvTNUWEYDI57a8iPh78BDjqMjSDQ" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "StatusEntryDto": { + "required": [ + "issuerId", + "purpose" + ], + "type": "object", + "properties": { + "purpose": { + "type": "string" + }, + "issuerId": { + "type": "string" + } + } + }, + "CredentialStatusDto": { + "required": [ + "id", + "statusListCredential", + "statusListIndex", + "statusPurpose", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "statusPurpose": { + "type": "string" + }, + "statusListIndex": { + "type": "string" + }, + "statusListCredential": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "Authenticate using access_token": { + "type": "apiKey", + "description": "**Bearer (apiKey)**\nJWT Authorization header using the Bearer scheme.\nEnter **Bearer** [space] and then your token in the text input below.\nExample: Bearer 12345abcdef", + "name": "Authorization", + "in": "header" + } + } + } +} From 190ed6ddc6a640484e35aad5f557fe971d687c59 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 13 Jun 2024 16:11:55 +0530 Subject: [PATCH 17/60] doc: uml added to issue and verify revocable VC --- README.md | 2 +- docs/arc42/images/issue_revocable_vc.png | Bin 0 -> 43766 bytes docs/arc42/images/verify_revocable_vc.png | Bin 0 -> 36718 bytes docs/arc42/main.md | 66 ++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 docs/arc42/images/issue_revocable_vc.png create mode 100644 docs/arc42/images/verify_revocable_vc.png diff --git a/README.md b/README.md index 244d2c3a..58d13261 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,4 @@ For end-to-end testing both the application should be running. 2. Run tests -``./gradlew clean test` +``./gradlew clean test`` diff --git a/docs/arc42/images/issue_revocable_vc.png b/docs/arc42/images/issue_revocable_vc.png new file mode 100644 index 0000000000000000000000000000000000000000..2174a51ed1c8c29259cad5000d7a4064ad8bedc4 GIT binary patch literal 43766 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

}FzZxz~AA&n0EDOuCI< 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 DYnFjH literal 0 HcmV?d00001 diff --git a/docs/arc42/main.md b/docs/arc42/main.md index ba5f7fe2..67422f20 100644 --- a/docs/arc42/main.md +++ b/docs/arc42/main.md @@ -177,6 +177,72 @@ group "Create Wallet" end group ``` +### Issue revocable credential + +![issue_revocable_vc.png](images%2Fissue_revocable_vc.png) + +``` +@startuml +title issue revocable VC +actor User as User +participant "Managed identity wallet" as MIW +participant "Revocation service" as RS +group "Issue VC" +User -> MIW: "/api/credentials/issuer" with revocable=true +alt "If revocable then get issuer status list" +MIW -> RS: "/api/v1/revocations/statusEntry +alt If status list VC not created? +RS -> RS: Create StatusList credentail +RS -> MIW: Sign new status list VC +MIW -> MIW: Save status list into issuer table +RS<-- MIW: return sighed status list VC +RS -> RS: Store StatusList VC in DB +end +RS -> RS: Get Current StatusList Index for Issuer +RS -> RS: Incease statusList Index by 1 and save in DB +RS --> MIW: Return Status List +end +group "Create and Sign VC" +MIW -> MIW: Create credentail +alt "If revocable then add status list" +MIW -> MIW: Add Status List in VC +end +MIW -> MIW: Sign VC +end group +MIW --> User: Return revocable VC +end group +@enduml + +``` + +### Verify revocable credential + +![verify_revocable_vc.png](images%2Fverify_revocable_vc.png) + +``` +@startuml +actor User as User +participant "Managed Identity Wallet" as MIW +participant "Revocation service" as RS +title Verify VC with revocation status +group "Verify VC" +User -> MIW: "/api/credentials/validation?withCredentialExpiryDate=true&withRevocation=true" +alt "If withRevocation then check issuer status list" +MIW -> RS: "/api/v1/revocations/credentials/{issueId}" +RS -> RS: Get Current StatusList Index for Issuer +RS --> MIW: Return StatusList VC +end +group "Credential Validation" +MIW -> MIW: validate status list VC +MIW -> MIW: Valaidate vc index with encoded list of status list VC +MIW -> MIW: Check Credential is not expired +MIW -> MIW: Validate Credential JsonLD +MIW -> MIW: Verify Credential Signature +end group +MIW --> User: Return Valid or Invalid + Reason +@enduml +``` + ### Validate Verifiable Presentation ```plantuml From ce29cb8287293a88745ec2c9bbb5d938564c568a Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Fri, 14 Jun 2024 10:14:03 +0530 Subject: [PATCH 18/60] fix: docker context path --- .github/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1751418f..f4d31cbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: - name: Push image uses: docker/build-push-action@v5 with: - context: . + context: ./miw push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -222,7 +222,7 @@ jobs: # Package MIW chart helm_package_path=$(helm package -u -d helm-charts ./charts/managed-identity-wallet | grep -o 'to: .*' | cut -d' ' -f2-) echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV - + # Commit and push to gh-pages git add helm-charts git stash -- helm-charts @@ -230,13 +230,13 @@ jobs: git fetch origin git checkout gh-pages git stash pop - + # Generate helm repo index.yaml helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ git add index.yaml - + git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" - + git push origin gh-pages - name: Upload chart to GitHub release From ec8bb008746ee901acccc8eaccda3e5793aea775 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Fri, 14 Jun 2024 18:46:23 +0530 Subject: [PATCH 19/60] feat: revoke API, revocation support in issue VC API, wallet-commons module for common classes --- build.gradle | 5 + miw/Dockerfile | 2 +- miw/build.gradle | 4 + .../ManagedIdentityWalletsApplication.java | 2 + .../IssuersCredentialControllerApiDocs.java | 7 +- .../apidocs/RevocationAPIDoc.java | 159 ++++++++++++++++++ .../config/ExceptionHandling.java | 4 +- .../config/RevocationSettings.java | 11 +- .../security/PresentationIatpFilter.java | 82 +++++++++ .../config/security/SecurityConfig.java | 6 +- .../constant/RestURI.java | 2 + .../HoldersCredentialController.java | 53 +++--- .../IssuersCredentialController.java | 63 ++++--- .../controller/RevocationController.java | 75 +++++++++ .../controller/SecureTokenController.java | 2 +- .../HoldersCredentialRepository.java | 20 --- .../domain/CredentialCreationConfig.java | 4 +- .../domain/PresentationCreationConfig.java | 2 +- .../dto/CreateWalletRequest.java | 2 +- .../dto/CredentialVerificationRequest.java | 4 +- .../dto/CredentialsResponse.java | 2 +- .../dto/StatusListRequest.java | 16 +- .../dto/ValidationResult.java | 2 +- .../exception/ForbiddenException.java | 62 ------- .../exception/RevocationException.java | 53 ++++++ .../revocation/RevocationClient.java | 95 +++++++++++ .../revocation/RevocationClientConfig.java | 43 +++++ .../revocation/RevocationErrorDecoder.java | 62 +++++++ .../service/CommonService.java | 4 +- .../service/DidDocumentResolverService.java | 6 +- .../service/HoldersCredentialService.java | 34 ++-- .../service/IdpAuthorization.java | 8 +- .../service/IssuersCredentialService.java | 70 +++++--- .../service/JwtPresentationES256KService.java | 14 +- .../service/PresentationService.java | 20 +-- .../service/STSTokenValidationService.java | 4 +- .../service/WalletKeyService.java | 5 +- .../service/WalletService.java | 26 ++- .../service/revocation/RevocationService.java | 125 ++++++++++++++ .../signing/KeyProvider.java | 5 +- .../signing/LocalKeyProvider.java | 2 +- .../signing/LocalSigningServiceImpl.java | 19 ++- .../utils/BpnValidator.java | 4 +- .../utils/CommonUtils.java | 5 +- .../utils/CustomSignedJWTVerifier.java | 4 +- .../utils/TokenParsingUtils.java | 8 +- .../utils/TokenValidationUtils.java | 2 +- .../utils/Validate.java | 154 ----------------- .../SecureTokenRequestValidator.java | 14 +- miw/src/main/resources/application.yaml | 20 ++- .../PresentationIatpFilterTest.java | 6 +- .../controller/SecureTokenControllerTest.java | 8 +- .../did/DidDocumentsTest.java | 6 +- .../domain/CredentialCreationConfigTest.java | 4 +- .../service/IssuersCredentialServiceTest.java | 22 ++- .../STSTokenValidationServiceTest.java | 2 +- .../utils/AuthenticationUtils.java | 2 +- .../utils/TestUtils.java | 6 +- .../utils/TokenValidationUtilsTest.java | 2 +- .../vc/HoldersCredentialTest.java | 16 +- .../vc/IssuersCredentialTest.java | 7 +- .../vc/PresentationValidationTest.java | 14 +- ...eCredentialIssuerEqualProofSignerTest.java | 8 +- .../vp/PresentationServiceTest.java | 6 +- .../vp/PresentationTest.java | 4 +- .../wallet/WalletTest.java | 32 ++-- revocation-service/build.gradle | 8 +- .../RevocationApiControllerApiDocs.java | 128 ++++++++++---- .../revocation/config/ExceptionHandling.java | 2 +- .../config/security/SecurityConfig.java | 2 +- .../constant/RevocationApiEndpoints.java | 1 + .../controllers/BaseController.java | 4 +- .../controllers/RevocationApiController.java | 20 ++- .../revocation/dto/CredentialStatusDto.java | 4 +- .../revocation/dto/StatusEntryDto.java | 4 +- .../services/RevocationService.java | 64 +++++++ .../revocation/utils/CommonUtils.java | 53 ++++++ .../src/main/resources/application.yaml | 8 +- .../revocation/TestUtil.java | 14 ++ .../config/ExceptionHandlingTest.java | 2 +- .../RevocationApiControllerTest.java | 18 +- .../revocation/domain/BPNTest.java | 19 +-- .../revocation/dto/StatusEntryDtoTest.java | 6 +- .../dto/StatusListCredentialSubjectTest.java | 2 +- .../revocation/jpa/StatusListIndexTest.java | 16 +- .../revocation/utils/ValidateTest.java | 49 ------ settings.gradle | 1 + wallet-commons/build.gradle | 43 +++++ .../commons}/constant/ApplicationRole.java | 4 +- .../commons/constant/CredentialStatus.java | 15 +- .../commons/constant/RevocationPurpose.java | 32 ++++ .../commons}/constant/StringPool.java | 18 +- .../constant/SupportedAlgorithms.java | 4 +- .../constant/TokenValidationErrors.java | 2 +- .../commons}/exception/BadDataException.java | 4 +- .../exception/ForbiddenException.java | 5 +- .../commons}/utils/Validate.java | 3 +- .../commons}/ValidateTest.java | 5 +- 98 files changed, 1445 insertions(+), 657 deletions(-) create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java rename revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java => miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java (78%) create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java rename revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java => miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/StatusListRequest.java (80%) delete mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/ForbiddenException.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/RevocationException.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClient.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClientConfig.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationErrorDecoder.java create mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/revocation/RevocationService.java delete mode 100644 miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/Validate.java create mode 100644 revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/CommonUtils.java delete mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java create mode 100644 wallet-commons/build.gradle rename {miw/src/main/java/org/eclipse/tractusx/managedidentitywallets => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/constant/ApplicationRole.java (92%) rename revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java (76%) create mode 100644 wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java rename {miw/src/main/java/org/eclipse/tractusx/managedidentitywallets => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/constant/StringPool.java (93%) rename {miw/src/main/java/org/eclipse/tractusx/managedidentitywallets => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/constant/SupportedAlgorithms.java (91%) rename {miw/src/main/java/org/eclipse/tractusx/managedidentitywallets => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/constant/TokenValidationErrors.java (94%) rename {miw/src/main/java/org/eclipse/tractusx/managedidentitywallets => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/exception/BadDataException.java (92%) rename {revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/exception/ForbiddenException.java (93%) rename {revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/utils/Validate.java (98%) rename {miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils => wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons}/ValidateTest.java (91%) diff --git a/build.gradle b/build.gradle index 34271e88..a0a9fac8 100644 --- a/build.gradle +++ b/build.gradle @@ -65,6 +65,11 @@ subprojects { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation "org.testcontainers:junit-jupiter" + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-engine' + } diff --git a/miw/Dockerfile b/miw/Dockerfile index 589b41d7..c92d9245 100644 --- a/miw/Dockerfile +++ b/miw/Dockerfile @@ -27,7 +27,7 @@ RUN apk add curl USER miw -COPY ../LICENSE NOTICE.md miw/DEPENDENCIES SECURITY.md ./build/libs/miw-latest.jar /app/ +COPY ../LICENSE ../NOTICE.md ../DEPENDENCIES ../SECURITY.md ./build/libs/miw-latest.jar /app/ WORKDIR /app diff --git a/miw/build.gradle b/miw/build.gradle index 7e4239c0..590d9f2f 100644 --- a/miw/build.gradle +++ b/miw/build.gradle @@ -40,6 +40,10 @@ configurations { dependencies { + + //project deps + implementation project(":wallet-commons") + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/ManagedIdentityWalletsApplication.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/ManagedIdentityWalletsApplication.java index f67053ad..9fc948c8 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/ManagedIdentityWalletsApplication.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/ManagedIdentityWalletsApplication.java @@ -24,6 +24,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.transaction.annotation.EnableTransactionManagement; /** @@ -32,6 +33,7 @@ @SpringBootApplication @ConfigurationPropertiesScan @EnableTransactionManagement +@EnableFeignClients public class ManagedIdentityWalletsApplication { /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java index 6f8571ef..5ad56101 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java @@ -615,11 +615,12 @@ public class IssuersCredentialControllerApiDocs { + "Setting this parameter to false will result in the VC being created as JSON-LD " + "Defaults to false if not specified.", examples = { - @ExampleObject(name = "Create VC as JWT", value = "true"), - @ExampleObject(name = "Do not create VC as JWT", value = "false") - }) + @ExampleObject(name = "Create VC as JWT", value = "true"), + @ExampleObject(name = "Do not create VC as JWT", value = "false") + }) @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface AsJwtParam { } + } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java new file mode 100644 index 00000000..ed03dbaf --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java @@ -0,0 +1,159 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.apidocs; + + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class RevocationAPIDoc { + + @Parameter(description = "Specifies whether the VC (Verifiable Credential) should revocable. The default value will be true") + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + public @interface Revocable { + + } + + @Tag(name = "Verifiable Credential - Revoke") + @ApiResponse(responseCode = "201", description = "Issuer credential", content = { + @Content(examples = { + @ExampleObject(name = "Success response", value = """ + { + "message": "Credential has been revoked" + } + """) + }) + }) + @ApiResponse(responseCode = "404", description = "Wallet not found with credential issuer", content = { @Content(examples = { + @ExampleObject(name = "Wallet not found with credential issuer", value = """ + { + "type": "about:blank", + "title": "Wallet not found for identifier web:did:localhost:BPN", + "status": 404, + "detail": "Error Details", + "instance": "API endpoint", + "properties": { + "timestamp": 1689762476720 + } + } + """) + }) }) + @ApiResponse(responseCode = "500", description = "Any other internal server error", content = { @Content(examples = { + @ExampleObject(name = "Internal server error", value = """ + { + "type": "about:blank", + "title": "Error Title", + "status": 500, + "detail": "Error Details", + "instance": "API endpoint", + "properties": { + "timestamp": 1689762476720 + } + } + """) + }) }) + @ApiResponse(responseCode = "409", description = "Credential is already revoked", content = { @Content(examples = { + @ExampleObject(name = "Credential is already revoked", value = """ + { + "type": "about:blank", + "title": "Revocation service error", + "status": 409, + "detail": "RevocationProblem: Credential already revoked", + "instance": "/api/credentials/revoke", + "properties": { + "timestamp": "1704438069232", + "type": "about:blank", + "title": "Revocation service error", + "status": "409", + "detail": "Credential already revoked", + "instance": "/api/v1/revocations/revoke" + } + } + """) + }) }) + @ApiResponse(responseCode = "403", description = "The request could not be completed due to a forbidden access", content = { @Content(examples = {}) }) + @io.swagger.v3.oas.annotations.parameters.RequestBody(content = { + @Content(examples = @ExampleObject(""" + { + "issuanceDate": "2023-12-26T10:58:02Z", + "credentialSubject": + [ + { + "holderIdentifier": "BPNL000000000002", + "id": "did:web:localhost:BPNL000000000002", + "type": "SummaryCredential", + "items": + [ + "BpnCredential" + ], + "contractTemplate": "https://public.catena-x.org/contracts/" + } + ], + "id": "did:web:localhost:BPNL000000000000#6b680abe-8869-435f-9d83-ad5ac336b8da", + "proof": + { + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "did:web:localhost:BPNL000000000000#a8233b68-f41e-4f14-8dff-fe16a63e0b19", + "created": "2023-12-26T10:58:02Z", + "jws": "eyJhbGciOiJFZERTQSJ9..uFqnCMbcOJneZDl7mCg8PeUjhWdUN53C8dB1E3EoWx7_hVgxsU8L7WkRYxvxIEa_DddViOoKs8E95ymYK081Aw" + }, + "type": + [ + "VerifiableCredential", + "SummaryCredential" + ], + "@context": + [ + "https://www.w3.org/2018/credentials/v1", + "https://cofinity-x.github.io/schema-registry/v1.1/SummaryVC.json", + "https://w3id.org/security/suites/jws-2020/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "issuer": "did:web:localhost:BPNL000000000000", + "expirationDate": "2024-12-31T18:30:00Z", + "credentialStatus": + { + "id": "did:web:localhost:BPNL000000000000#1", + "statusPurpose": "revocation", + "statusListIndex": "1", + "statusListCredential": "https://7337-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials?issuerId=did:web:localhost:BPNL000000000000", + "type": "BitstringStatusListEntry" + } + } + """)) + }) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface RevokeCredentialDoc { + + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java index 95b00550..3e73396a 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java @@ -26,10 +26,10 @@ import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.exception.CredentialNotFoundProblem; import org.eclipse.tractusx.managedidentitywallets.exception.DuplicateWalletProblem; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.exception.MissingVcTypesException; import org.eclipse.tractusx.managedidentitywallets.exception.PermissionViolationException; import org.eclipse.tractusx.managedidentitywallets.exception.WalletNotFoundProblem; diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java similarity index 78% rename from revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java rename to miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java index 00f90ef8..c36bb2ec 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/PurposeType.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java @@ -19,9 +19,12 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.revocation.constant; +package org.eclipse.tractusx.managedidentitywallets.config; -public enum PurposeType { - REVOCATION, - SUSPENSION, +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.net.URI; + +@ConfigurationProperties(prefix = "miw.revocation") +public record RevocationSettings(URI url) { } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java new file mode 100644 index 00000000..6099e12a --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/PresentationIatpFilter.java @@ -0,0 +1,82 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.config.security; + +import io.micrometer.common.util.StringUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; +import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +public class PresentationIatpFilter extends GenericFilterBean { + + RequestMatcher customFilterUrl = new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_IATP); + + STSTokenValidationService validationService; + + public PresentationIatpFilter(STSTokenValidationService validationService) { + this.validationService = validationService; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + + if (customFilterUrl.matches(httpServletRequest)) { + String authHeader = httpServletRequest.getHeader("Authorization"); + if (StringUtils.isEmpty(authHeader)) { + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } else { + ValidationResult result = validationService.validateToken(authHeader); + if (!result.isValid()) { + List errorValues = new ArrayList<>(); + result.getErrors().forEach(c -> errorValues.add(c.name())); + String content = String.join(StringPool.COMA_SEPARATOR, errorValues); + + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.setContentLength(content.length()); + httpServletResponse.getWriter().write(content); + } else { + chain.doFilter(request, response); + } + } + } else { + chain.doFilter(request, response); + } + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java index a0a6178e..708a1f37 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/SecurityConfig.java @@ -23,7 +23,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.tractusx.managedidentitywallets.constant.ApplicationRole; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.ApplicationRole; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; import org.eclipse.tractusx.managedidentitywallets.utils.BpnValidator; @@ -56,6 +56,7 @@ import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.PUT; /** * The type Security config. @@ -115,6 +116,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //VP - Validation .requestMatchers(new AntPathRequestMatcher(RestURI.API_PRESENTATIONS_VALIDATION, POST.name())).hasAnyRole(ApplicationRole.ROLE_VIEW_WALLETS, ApplicationRole.ROLE_VIEW_WALLET) //validate VP + //VC - revoke + .requestMatchers(new AntPathRequestMatcher(RestURI.CREDENTIALS_REVOKE, PUT.name())).hasAnyRole(ApplicationRole.ROLE_UPDATE_WALLET, ApplicationRole.ROLE_UPDATE_WALLETS) //revoke credentials + //VC - Holder .requestMatchers(new AntPathRequestMatcher(RestURI.CREDENTIALS, GET.name())).hasAnyRole(ApplicationRole.ROLE_VIEW_WALLET, ApplicationRole.ROLE_VIEW_WALLETS) //get credentials .requestMatchers(new AntPathRequestMatcher(RestURI.CREDENTIALS, POST.name())).hasAnyRole(ApplicationRole.ROLE_UPDATE_WALLET, ApplicationRole.ROLE_UPDATE_WALLETS) //issue credential diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java index 97e73c97..27c8ad27 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/RestURI.java @@ -62,6 +62,8 @@ private RestURI() { */ public static final String CREDENTIALS_VALIDATION = "/api/credentials/validation"; + public static final String CREDENTIALS_REVOKE = "/api/credentials/revoke"; + /** * The constant ISSUERS_CREDENTIALS. */ diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java index 8d85d87d..71495142 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java @@ -32,13 +32,15 @@ import org.eclipse.tractusx.managedidentitywallets.apidocs.HoldersCredentialControllerApiDocs.GetCredentialsApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.HoldersCredentialControllerApiDocs.IssueCredentialApiDoc; import org.eclipse.tractusx.managedidentitywallets.apidocs.IssuersCredentialControllerApiDocs.AsJwtParam; +import org.eclipse.tractusx.managedidentitywallets.apidocs.RevocationAPIDoc; import org.eclipse.tractusx.managedidentitywallets.command.GetCredentialsCommand; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.HoldersCredentialService; import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -46,6 +48,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -74,27 +77,26 @@ public class HoldersCredentialController { * @param sortTpe the sort tpe * @param authentication the authentication * @return the credentials - */ + */ @GetCredentialsApiDocs @GetMapping(path = RestURI.CREDENTIALS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCredentials(@Parameter(name = "credentialId", description = "Credential Id", examples = {@ExampleObject(name = "Credential Id", value = "did:web:localhost:BPNL000000000000#12528899-160a-48bd-ba15-f396c3959ae9")}) @RequestParam(required = false) String credentialId, - @Parameter(name = "issuerIdentifier", description = "Issuer identifier(did of BPN)", examples = {@ExampleObject(name = "bpn", value = "BPNL000000000000", description = "bpn"), @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(required = false) String issuerIdentifier, - @Parameter(name = "type", description = "Type of VC", examples = {@ExampleObject(name = "SummaryCredential", value = "SummaryCredential", description = "SummaryCredential"), @ExampleObject(description = "BpnCredential", name = "BpnCredential", value = "BpnCredential")}) @RequestParam(required = false) List type, - @Parameter(name = "sortColumn", description = "Sort column name", - examples = { - @ExampleObject(value = "createdAt", name = "creation date"), - @ExampleObject(value = "issuerDid", name = "Issuer did"), - @ExampleObject(value = "type", name = "Credential type"), - @ExampleObject(value = "credentialId", name = "Credential id"), - @ExampleObject(value = "selfIssued", name = "Self issued credential"), - @ExampleObject(value = "stored", name = "Stored credential") - } - ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, - @Parameter(name = "sortTpe", description = "Sort order", examples = {@ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order")}) @RequestParam(required = false, defaultValue = "desc") String sortTpe, - @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Page number, Page number start with zero") @RequestParam(required = false, defaultValue = "0") int pageNumber, - @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, - @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt, - + public ResponseEntity> getCredentials(@Parameter(name = "credentialId", description = "Credential Id", examples = { @ExampleObject(name = "Credential Id", value = "did:web:localhost:BPNL000000000000#12528899-160a-48bd-ba15-f396c3959ae9") }) @RequestParam(required = false) String credentialId, + @Parameter(name = "issuerIdentifier", description = "Issuer identifier(did of BPN)", examples = { @ExampleObject(name = "bpn", value = "BPNL000000000000", description = "bpn"), @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000") }) @RequestParam(required = false) String issuerIdentifier, + @Parameter(name = "type", description = "Type of VC", examples = { @ExampleObject(name = "SummaryCredential", value = "SummaryCredential", description = "SummaryCredential"), @ExampleObject(description = "BpnCredential", name = "BpnCredential", value = "BpnCredential") }) @RequestParam(required = false) List type, + @Parameter(name = "sortColumn", description = "Sort column name", + examples = { + @ExampleObject(value = "createdAt", name = "creation date"), + @ExampleObject(value = "issuerDid", name = "Issuer did"), + @ExampleObject(value = "type", name = "Credential type"), + @ExampleObject(value = "credentialId", name = "Credential id"), + @ExampleObject(value = "selfIssued", name = "Self issued credential"), + @ExampleObject(value = "stored", name = "Stored credential") + } + ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, + @Parameter(name = "sortTpe", description = "Sort order", examples = { @ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order") }) @RequestParam(required = false, defaultValue = "desc") String sortTpe, + @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Page number, Page number start with zero") @RequestParam(required = false, defaultValue = "0") int pageNumber, + @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, + @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt, Authentication authentication) { log.debug("Received request to get credentials. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); final GetCredentialsCommand command; @@ -116,17 +118,20 @@ public ResponseEntity> getCredentials(@Parameter(n /** * Issue credential response entity. * - * @param data the data - * @param authentication the authentication + * @param data the data + * @param authentication the authentication * @return the response entity */ @IssueCredentialApiDoc @PostMapping(path = RestURI.CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity issueCredential(@RequestBody Map data, Authentication authentication, - @AsJwtParam @RequestParam(name = "asJwt", defaultValue = "true") boolean asJwt + @AsJwtParam @RequestParam(name = "asJwt", defaultValue = "true") boolean asJwt, + @RevocationAPIDoc.Revocable @RequestParam(name = StringPool.REVOCABLE, defaultValue = "true") boolean revocable, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token ) { log.debug("Received request to issue credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); - return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, TokenParsingUtils.getBPNFromToken(authentication), asJwt)); + return ResponseEntity.status(HttpStatus.CREATED).body(holdersCredentialService.issueCredential(data, TokenParsingUtils.getBPNFromToken(authentication), asJwt, revocable, token)); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java index 55592d09..42f003e9 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java @@ -31,14 +31,16 @@ import org.eclipse.tractusx.managedidentitywallets.apidocs.IssuersCredentialControllerApiDocs.GetCredentialsApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.IssuersCredentialControllerApiDocs.IssueVerifiableCredentialUsingBaseWalletApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.IssuersCredentialControllerApiDocs.ValidateVerifiableCredentialApiDocs; +import org.eclipse.tractusx.managedidentitywallets.apidocs.RevocationAPIDoc; import org.eclipse.tractusx.managedidentitywallets.command.GetCredentialsCommand; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -46,6 +48,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -60,15 +63,6 @@ @Slf4j public class IssuersCredentialController { - /** - * The constant API_TAG_VERIFIABLE_CREDENTIAL_ISSUER. - */ - public static final String API_TAG_VERIFIABLE_CREDENTIAL_ISSUER = "Verifiable Credential - Issuer"; - /** - * The constant API_TAG_VERIFIABLE_CREDENTIAL_VALIDATION. - */ - public static final String API_TAG_VERIFIABLE_CREDENTIAL_VALIDATION = "Verifiable Credential - Validation"; - private final IssuersCredentialService issuersCredentialService; @@ -87,19 +81,19 @@ public class IssuersCredentialController { */ @GetCredentialsApiDocs @GetMapping(path = RestURI.ISSUERS_CREDENTIALS, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getCredentials(@Parameter(name = "credentialId", description = "Credential Id", examples = {@ExampleObject(name = "Credential Id", value = "did:web:localhost:BPNL000000000000#12528899-160a-48bd-ba15-f396c3959ae9")}) @RequestParam(required = false) String credentialId, - @Parameter(name = "holderIdentifier", description = "Holder identifier(did of BPN)", examples = {@ExampleObject(name = "bpn", value = "BPNL000000000001", description = "bpn"), @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000001")}) @RequestParam(required = false) String holderIdentifier, - @Parameter(name = "type", description = "Type of VC", examples = {@ExampleObject(name = "SummaryCredential", value = "SummaryCredential", description = "SummaryCredential"), @ExampleObject(description = "BpnCredential", name = "BpnCredential", value = "BpnCredential")}) @RequestParam(required = false) List type, - @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Page number, Page number start with zero") @RequestParam(required = false, defaultValue = "0") int pageNumber, - @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, - @Parameter(name = "sortColumn", description = "Sort column name", - examples = { - @ExampleObject(value = "createdAt", name = "creation date"), - @ExampleObject(value = "holderDid", name = "Holder did"), - @ExampleObject(value = "type", name = "Credential type"), - @ExampleObject(value = "credentialId", name = "Credential id") - } - ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, + public ResponseEntity> getCredentials(@Parameter(name = "credentialId", description = "Credential Id", examples = { @ExampleObject(name = "Credential Id", value = "did:web:localhost:BPNL000000000000#12528899-160a-48bd-ba15-f396c3959ae9") }) @RequestParam(required = false) String credentialId, + @Parameter(name = "holderIdentifier", description = "Holder identifier(did of BPN)", examples = { @ExampleObject(name = "bpn", value = "BPNL000000000001", description = "bpn"), @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000001") }) @RequestParam(required = false) String holderIdentifier, + @Parameter(name = "type", description = "Type of VC", examples = { @ExampleObject(name = "SummaryCredential", value = "SummaryCredential", description = "SummaryCredential"), @ExampleObject(description = "BpnCredential", name = "BpnCredential", value = "BpnCredential") }) @RequestParam(required = false) List type, + @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Page number, Page number start with zero") @RequestParam(required = false, defaultValue = "0") int pageNumber, + @Min(0) @Max(Integer.MAX_VALUE) @Parameter(description = "Number of records per page") @RequestParam(required = false, defaultValue = Integer.MAX_VALUE + "") int size, + @Parameter(name = "sortColumn", description = "Sort column name", + examples = { + @ExampleObject(value = "createdAt", name = "creation date"), + @ExampleObject(value = "holderDid", name = "Holder did"), + @ExampleObject(value = "type", name = "Credential type"), + @ExampleObject(value = "credentialId", name = "Credential id") + } + ) @RequestParam(required = false, defaultValue = "createdAt") String sortColumn, @Parameter(name = "sortTpe", description = "Sort order", examples = { @ExampleObject(value = "desc", name = "Descending order"), @ExampleObject(value = "asc", name = "Ascending order") }) @RequestParam(required = false, defaultValue = "desc") String sortTpe, @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt, Authentication authentication) { @@ -123,31 +117,36 @@ public ResponseEntity> getCredentials(@Parameter(n /** * Credentials validation response entity. * - * @param credentialVerificationRequest the request - * @param withCredentialExpiryDate the with credential expiry date + * @param credentialVerificationRequest the request + * @param withCredentialExpiryDate the with credential expiry date * @return the response entity */ @PostMapping(path = RestURI.CREDENTIALS_VALIDATION, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @ValidateVerifiableCredentialApiDocs public ResponseEntity> credentialsValidation(@RequestBody CredentialVerificationRequest credentialVerificationRequest, - @Parameter(description = "Check expiry of VC") @RequestParam(name = "withCredentialExpiryDate", defaultValue = "false", required = false) boolean withCredentialExpiryDate) { + @Parameter(description = "Check expiry of VC") @RequestParam(name = "withCredentialExpiryDate", defaultValue = "false", required = false) boolean withCredentialExpiryDate, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token) { log.debug("Received request to validate verifiable credentials"); - return ResponseEntity.status(HttpStatus.OK).body(issuersCredentialService.credentialsValidation(credentialVerificationRequest, withCredentialExpiryDate)); + return ResponseEntity.status(HttpStatus.OK).body(issuersCredentialService.credentialsValidation(credentialVerificationRequest, withCredentialExpiryDate, token)); } /** * Issue credential response entity. * - * @param holderDid the holder did - * @param data the data + * @param holderDid the holder did + * @param data the data * @param authentication the authentication * @return the response entity */ @PostMapping(path = RestURI.ISSUERS_CREDENTIALS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @IssueVerifiableCredentialUsingBaseWalletApiDocs - public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = {@ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Authentication authentication, - @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt) { + + + public ResponseEntity issueCredentialUsingBaseWallet(@Parameter(description = "Holder DID", examples = { @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000") }) @RequestParam(name = "holderDid") String holderDid, @RequestBody Map data, Authentication authentication, + @AsJwtParam @RequestParam(name = StringPool.AS_JWT, defaultValue = "true") boolean asJwt, + @RevocationAPIDoc.Revocable @RequestParam(name = StringPool.REVOCABLE, defaultValue = "true") boolean revocable, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token) { log.debug("Received request to issue verifiable credential. BPN: {}", TokenParsingUtils.getBPNFromToken(authentication)); - return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, TokenParsingUtils.getBPNFromToken(authentication))); + return ResponseEntity.status(HttpStatus.CREATED).body(issuersCredentialService.issueCredentialUsingBaseWallet(holderDid, data, asJwt, revocable, TokenParsingUtils.getBPNFromToken(authentication), token)); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java new file mode 100644 index 00000000..7647d1a3 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java @@ -0,0 +1,75 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.controller; + + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.apidocs.IssuersCredentialControllerApiDocs; +import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; +import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.util.Map; + +/** + * The type Revocation controller. + */ +@RestController +@Slf4j +@RequiredArgsConstructor +@Tag(name = "Verifiable Credential - Revoke") +public class RevocationController extends BaseController { + + + private final RevocationService revocationService; + + + /** + * Revoke credential . + * + * @param credentialVerificationRequest the credential verification request + * @param token the token + * @param principal the principal + * @return the response entity + */ + @PutMapping(path = RestURI.CREDENTIALS_REVOKE, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @IssuersCredentialControllerApiDocs.ValidateVerifiableCredentialApiDocs + public ResponseEntity> revokeCredential(@RequestBody CredentialVerificationRequest credentialVerificationRequest, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token, Principal principal) { + revocationService.revokeCredential(credentialVerificationRequest, getBPNFromToken(principal), token); + return ResponseEntity.status(HttpStatus.OK).body(Map.of("message", "Credential has been revoked")); + + } + +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java index 3cfd7865..641378a3 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenController.java @@ -30,7 +30,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.tractusx.managedidentitywallets.apidocs.SecureTokenControllerApiDoc; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.domain.BusinessPartnerNumber; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java index f3459d9e..ee462db3 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java @@ -58,24 +58,4 @@ public interface HoldersCredentialRepository extends BaseRepository getByHolderDidAndType(String holderDid, String type); - - List getByHolderDidAndIssuerDidAndTypeAndStored(String holderDid, String issuerDid, String type, boolean stored); - - /** - * Exists by holder did and type boolean. - * - * @param holderDid the holder did - * @param type the type - * @return the boolean - */ - boolean existsByHolderDidAndType(String holderDid, String type); - - /** - * Exists by holder did and credential id boolean. - * - * @param holderDid the holder did - * @param credentialId the credential id - * @return the boolean - */ - boolean existsByHolderDidAndCredentialId(String holderDid, String credentialId); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfig.java index f75ae464..0661ae1c 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfig.java @@ -25,7 +25,7 @@ import lombok.Getter; import lombok.NonNull; import lombok.Setter; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatus; @@ -65,6 +65,8 @@ public class CredentialCreationConfig { private boolean selfIssued; + private boolean revocable; + // this will be used by the DB-Impl of storage to retrieve privateKey @NonNull private String keyName; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/PresentationCreationConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/PresentationCreationConfig.java index 0ff581e0..b2479f4a 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/PresentationCreationConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/domain/PresentationCreationConfig.java @@ -24,7 +24,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NonNull; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.ssi.lib.model.did.Did; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CreateWalletRequest.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CreateWalletRequest.java index 4d5b253d..b2038989 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CreateWalletRequest.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CreateWalletRequest.java @@ -29,7 +29,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialVerificationRequest.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialVerificationRequest.java index c4f9dd4a..73d15872 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialVerificationRequest.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialVerificationRequest.java @@ -21,11 +21,11 @@ package org.eclipse.tractusx.managedidentitywallets.dto; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import java.util.LinkedHashMap; import java.util.Map; - + public class CredentialVerificationRequest extends LinkedHashMap { diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialsResponse.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialsResponse.java index d90d7e44..d1dc365a 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialsResponse.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/CredentialsResponse.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.dto; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import java.util.LinkedHashMap; import java.util.Map; diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/StatusListRequest.java similarity index 80% rename from revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java rename to miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/StatusListRequest.java index 1aeb1f42..2c6c2e3f 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/StatusListRequest.java @@ -19,9 +19,19 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.revocation.utils; +package org.eclipse.tractusx.managedidentitywallets.dto; -public class StringPool { - public static final String BPN = "bpn"; +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StatusListRequest { + + private String issuerId; + + private String purpose; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/ValidationResult.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/ValidationResult.java index 8929fada..97700284 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/ValidationResult.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dto/ValidationResult.java @@ -26,7 +26,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors; import java.util.List; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/ForbiddenException.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/ForbiddenException.java deleted file mode 100644 index 51938e2b..00000000 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/ForbiddenException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ****************************************************************************** - */ - -package org.eclipse.tractusx.managedidentitywallets.exception; - -/** - * The type Forbidden exception. - */ -public class ForbiddenException extends RuntimeException { - - /** - * Instantiates a new Forbidden exception. - */ - public ForbiddenException() { - } - - /** - * Instantiates a new Forbidden exception. - * - * @param message the message - */ - public ForbiddenException(String message) { - super(message); - } - - /** - * Instantiates a new Forbidden exception. - * - * @param message the message - * @param cause the cause - */ - public ForbiddenException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Instantiates a new Forbidden exception. - * - * @param cause the cause - */ - public ForbiddenException(Throwable cause) { - super(cause); - } -} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/RevocationException.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/RevocationException.java new file mode 100644 index 00000000..9b11bbf8 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/RevocationException.java @@ -0,0 +1,53 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.exception; + +import lombok.Getter; + +import java.util.Map; + +/** + * The type Revocation problem. + */ +@Getter +public class RevocationException extends RuntimeException { + + + private final int status; + + private final String message; + + private final Map details; + + /** + * Instantiates a new Revocation problem. + * + * @param status the status + * @param message the message + */ + public RevocationException(int status, String message, Map details) { + super(message); + this.status = status; + this.message = message; + this.details = details; + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClient.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClient.java new file mode 100644 index 00000000..6d27e8af --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClient.java @@ -0,0 +1,95 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation; + +import io.swagger.v3.oas.annotations.Parameter; +import org.eclipse.tractusx.managedidentitywallets.dto.StatusListRequest; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatus; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.Map; + +/** + * The interface Revocation client. + */ +@FeignClient(value = "RevocationService", url = "${miw.revocation.url}", configuration = RevocationClientConfig.class) +public interface RevocationClient { + + /** + * Gets status list credential. + * + * @param issuerBpn the issuer BPN + * @param status the status + * @param index the index + * @param token the token + * @return the status list credential + */ + @GetMapping(path = "/api/v1/revocations/credentials/{issuerBpn}/{status}/{index}", produces = MediaType.APPLICATION_JSON_VALUE) + VerifiableCredential getStatusListCredential(@PathVariable(name = "issuerBpn") String issuerBpn, + @PathVariable(name = "status") String status, + @PathVariable(name = "index") String index, + @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token); + + + /** + * Gets status list entry. + * + * @param statusListRequest the status list request + * @param token the token + * @return the status list entry + */ + @PostMapping(path = "/api/v1/revocations/status-entry", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + Map getStatusListEntry(@RequestBody StatusListRequest statusListRequest, + @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token); + + + /** + * Revoke credential. + * + * @param verifiableCredentialStatus the verifiable credential status + * @param token the token + */ + @PostMapping(path = "/api/v1/revocations/revoke", consumes = MediaType.APPLICATION_JSON_VALUE) + void revokeCredential(@RequestBody VerifiableCredentialStatus verifiableCredentialStatus, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token); + + + /** + * Verify credential status map. + * + * @param verifiableCredentialStatus the verifiable credential status + * @param token the token + * @return the map + */ + @PostMapping(path = "/api/v1/revocations/verify", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + Map verifyCredentialStatus(@RequestBody VerifiableCredentialStatus verifiableCredentialStatus, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token); + +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClientConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClientConfig.java new file mode 100644 index 00000000..b440bc8a --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationClientConfig.java @@ -0,0 +1,43 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation; + +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The type Revocation client config. + */ +@Configuration +public class RevocationClientConfig { + + /** + * Error decoder error decoder. + * + * @return the error decoder + */ + @Bean + public ErrorDecoder errorDecoder() { + return new RevocationErrorDecoder(); + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationErrorDecoder.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationErrorDecoder.java new file mode 100644 index 00000000..572f1a22 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/RevocationErrorDecoder.java @@ -0,0 +1,62 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.Response; +import feign.codec.ErrorDecoder; +import lombok.SneakyThrows; +import org.eclipse.tractusx.managedidentitywallets.exception.RevocationException; +import org.springframework.http.HttpStatus; +import org.springframework.util.StreamUtils; + +import java.nio.charset.Charset; +import java.util.Map; + +/** + * The type Revocation error decoder. + */ +public class RevocationErrorDecoder implements ErrorDecoder { + @SneakyThrows + @Override + public Exception decode(String methodKey, Response response) { + Response.Body responseBody = response.body(); + HttpStatus responseStatus = HttpStatus.valueOf(response.status()); + if (responseBody != null && response.body() != null) { + String data = StreamUtils.copyToString(response.body().asInputStream(), Charset.defaultCharset()); + if (responseStatus.value() == HttpStatus.CONFLICT.value()) { + ObjectMapper objectMapper = new ObjectMapper(); + Map map = objectMapper.readValue(data, Map.class); + if (map.containsKey("detail")) { + throw new RevocationException(responseStatus.value(), map.get("detail").toString(), map); + } else { + throw new RevocationException(responseStatus.value(), data, map); + } + } else { + throw new RevocationException(responseStatus.value(), data, Map.of()); + + } + } else { + throw new RevocationException(responseStatus.value(), "Error in revocation service", Map.of()); + } + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/CommonService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/CommonService.java index 1acf3215..8e6228c3 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/CommonService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/CommonService.java @@ -23,12 +23,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.exception.WalletNotFoundProblem; import org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.springframework.stereotype.Service; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/DidDocumentResolverService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/DidDocumentResolverService.java index 3ffcccf8..eccb6b11 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/DidDocumentResolverService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/DidDocumentResolverService.java @@ -33,7 +33,7 @@ @Service public class DidDocumentResolverService { - final static HttpClient httpClient = HttpClient.newHttpClient(); + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); @Getter private final DidWebResolver didDocumentResolverRegistry; @@ -47,10 +47,10 @@ public DidDocumentResolverService(MIWSettings miwSettings) { final DidWebParser didParser = new DidWebParser(); didDocumentResolverRegistry = - new DidWebResolver(httpClient, didParser, enforceHttps); + new DidWebResolver(HTTP_CLIENT, didParser, enforceHttps); compositeDidResolver = new CompositeDidResolver( - new DidWebResolver(httpClient, didParser, enforceHttps) + new DidWebResolver(HTTP_CLIENT, didParser, enforceHttps) ); } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java index a69ad465..4eae8b25 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java @@ -33,8 +33,10 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; import org.eclipse.tractusx.managedidentitywallets.command.GetCredentialsCommand; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.HoldersCredentialRepository; @@ -42,13 +44,12 @@ import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; import org.eclipse.tractusx.managedidentitywallets.domain.VerifiableEncoding; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; -import org.eclipse.tractusx.managedidentitywallets.exception.CredentialNotFoundProblem; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.signing.SignerResult; import org.eclipse.tractusx.managedidentitywallets.signing.SigningService; import org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatusList2021Entry; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.stereotype.Service; @@ -81,6 +82,8 @@ public class HoldersCredentialService extends BaseService availableSigningServices; + private final RevocationService revocationService; + @Override protected BaseRepository getRepository() { @@ -172,7 +175,7 @@ public PageImpl getCredentials(GetCredentialsCommand comman * @param asJwt the as jwt * @return the verifiable credential */ - public CredentialsResponse issueCredential(Map data, String callerBpn, boolean asJwt) { + public CredentialsResponse issueCredential(Map data, String callerBpn, boolean asJwt, boolean revocable, String token) { VerifiableCredential verifiableCredential = new VerifiableCredential(data); Wallet issuerWallet = commonService.getWalletByIdentifier(verifiableCredential.getIssuer().toString()); @@ -185,7 +188,7 @@ public CredentialsResponse issueCredential(Map data, String call expiryDate = Date.from(verifiableCredential.getExpirationDate()); } - CredentialCreationConfig holdersCredentialCreationConfig = CredentialCreationConfig.builder() + CredentialCreationConfig.CredentialCreationConfigBuilder builder = CredentialCreationConfig.builder() .encoding(VerifiableEncoding.JSON_LD) .subject(verifiableCredential.getCredentialSubject().get(0)) .types(verifiableCredential.getTypes()) @@ -194,12 +197,19 @@ public CredentialsResponse issueCredential(Map data, String call .contexts(verifiableCredential.getContext()) .expiryDate(expiryDate) .selfIssued(true) + .revocable(revocable) .keyName(issuerWallet.getBpn()) - .algorithm(SupportedAlgorithms.valueOf(issuerWallet.getAlgorithm())) - .build(); + .algorithm(SupportedAlgorithms.valueOf(issuerWallet.getAlgorithm())); - // Create Credential + if (revocable) { + //get credential status in case of revocation + VerifiableCredentialStatusList2021Entry statusListEntry = revocationService.getStatusListEntry(issuerWallet.getBpn(), token); + builder.verifiableCredentialStatus(statusListEntry); + } + + CredentialCreationConfig holdersCredentialCreationConfig = builder.build(); + // Create Credential SignerResult signerResult = availableSigningServices.get(issuerWallet.getSigningServiceType()).createCredential(holdersCredentialCreationConfig); VerifiableCredential vc = (VerifiableCredential) signerResult.getJsonLd(); HoldersCredential credential = CommonUtils.convertVerifiableCredential(vc, holdersCredentialCreationConfig); @@ -224,8 +234,4 @@ public CredentialsResponse issueCredential(Map data, String call return cr; } - - private void isCredentialExistWithId(String holderDid, String credentialId) { - Validate.isFalse(holdersCredentialRepository.existsByHolderDidAndCredentialId(holderDid, credentialId)).launch(new CredentialNotFoundProblem("Credential ID: " + credentialId + " is not exists ")); - } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java index 956a938c..da0e819f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IdpAuthorization.java @@ -22,11 +22,12 @@ package org.eclipse.tractusx.managedidentitywallets.service; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.security.SecurityConfigProperties; import org.eclipse.tractusx.managedidentitywallets.domain.IdpTokenResponse; import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest; -import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedGrantTypeException; import org.eclipse.tractusx.managedidentitywallets.exception.InvalidIdpTokenResponseException; +import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedGrantTypeException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpHeaders; @@ -37,7 +38,6 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.CLIENT_CREDENTIALS; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_SECRET; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.GRANT_TYPE; @@ -64,11 +64,11 @@ public IdpAuthorization(final SecurityConfigProperties properties, final RestTem public IdpTokenResponse fromSecureTokenRequest(SecureTokenRequest secureTokenRequest) throws UnsupportedGrantTypeException, InvalidIdpTokenResponseException { // we're ignoring the input, but the protocol requires us to check. - if (!secureTokenRequest.getGrantType().equals(CLIENT_CREDENTIALS)) { + if (!secureTokenRequest.getGrantType().equals(StringPool.CLIENT_CREDENTIALS)) { throw new UnsupportedGrantTypeException("The provided 'grant_type' is not valid. Use 'client_credentials'."); } MultiValueMap tokenRequest = new LinkedMultiValueMap<>(); - tokenRequest.add(GRANT_TYPE, CLIENT_CREDENTIALS); + tokenRequest.add(GRANT_TYPE, StringPool.CLIENT_CREDENTIALS); tokenRequest.add(SCOPE, OPENID); tokenRequest.add(CLIENT_ID, secureTokenRequest.getClientId()); tokenRequest.add(CLIENT_SECRET, secureTokenRequest.getClientSecret()); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java index 8d6fb6d2..dba5f6a9 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java @@ -36,9 +36,12 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; import org.eclipse.tractusx.managedidentitywallets.command.GetCredentialsCommand; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.IssuersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -49,11 +52,10 @@ import org.eclipse.tractusx.managedidentitywallets.domain.VerifiableEncoding; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.signing.SignerResult; import org.eclipse.tractusx.managedidentitywallets.signing.SigningService; import org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; import org.eclipse.tractusx.ssi.lib.did.web.DidWebResolver; import org.eclipse.tractusx.ssi.lib.did.web.util.DidWebParser; @@ -61,6 +63,7 @@ import org.eclipse.tractusx.ssi.lib.jwt.SignedJwtValidator; import org.eclipse.tractusx.ssi.lib.jwt.SignedJwtVerifier; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatusList2021Entry; import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofValidation; import org.eclipse.tractusx.ssi.lib.serialization.SerializeUtil; import org.springframework.beans.factory.annotation.Autowired; @@ -78,8 +81,10 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.TreeMap; /** @@ -108,6 +113,8 @@ public class IssuersCredentialService extends BaseService availableSigningServices; + private final RevocationService revocationService; + @Override protected BaseRepository getRepository() { @@ -190,18 +197,19 @@ public PageImpl getCredentials(GetCredentialsCommand comman } - /** * Issue credential using base wallet * * @param holderDid the holder did * @param data the data * @param asJwt the as jwt + * @param revocable the revocable * @param callerBpn the caller bpn + * @param token the token * @return the verifiable credential */ @Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRED) - public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map data, boolean asJwt, String callerBpn) { + public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map data, boolean asJwt, boolean revocable, String callerBpn, String token) { //Fetch Holder Wallet Wallet holderWallet = commonService.getWalletByIdentifier(holderDid); @@ -213,7 +221,8 @@ public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map< boolean isSelfIssued = isSelfIssued(holderWallet.getBpn()); - CredentialCreationConfig holdersCredentialCreationConfig = CredentialCreationConfig.builder() + + CredentialCreationConfig.CredentialCreationConfigBuilder builder = CredentialCreationConfig.builder() .encoding(VerifiableEncoding.JSON_LD) .subject(verifiableCredential.getCredentialSubject().get(0)) .types(verifiableCredential.getTypes()) @@ -223,9 +232,16 @@ public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map< .contexts(verifiableCredential.getContext()) .expiryDate(Date.from(verifiableCredential.getExpirationDate())) .selfIssued(isSelfIssued) - .algorithm(SupportedAlgorithms.valueOf(issuerWallet.getAlgorithm())) - .build(); + .revocable(revocable) + .algorithm(SupportedAlgorithms.valueOf(issuerWallet.getAlgorithm())); + if (revocable) { + //get credential status in case of revocation + VerifiableCredentialStatusList2021Entry statusListEntry = revocationService.getStatusListEntry(issuerWallet.getBpn(), token); + builder.verifiableCredentialStatus(statusListEntry); + } + + CredentialCreationConfig holdersCredentialCreationConfig = builder.build(); // Create Credential SignerResult result = availableSigningServices.get(issuerWallet.getSigningServiceType()).createCredential(holdersCredentialCreationConfig); @@ -258,7 +274,7 @@ public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map< } - private JWTVerificationResult verifyVCAsJWT(String jwt, DidResolver didResolver, boolean withCredentialsValidation, boolean withCredentialExpiryDate) throws IOException, ParseException { + private JWTVerificationResult verifyVCAsJWT(String jwt, DidResolver didResolver, boolean withCredentialsValidation, boolean withCredentialExpiryDate, String token) throws IOException, ParseException { SignedJWT signedJWT = SignedJWT.parse(jwt); Map claims = objectMapper.readValue(signedJWT.getPayload().toBytes(), Map.class); String vcClaim = objectMapper.writeValueAsString(claims.get("vc")); @@ -266,7 +282,10 @@ private JWTVerificationResult verifyVCAsJWT(String jwt, DidResolver didResolver, VerifiableCredential verifiableCredential = new VerifiableCredential(map); //took this approach to avoid issues in sonarQube - return new JWTVerificationResult(validateSignature(withCredentialsValidation, signedJWT, didResolver) && validateJWTExpiryDate(withCredentialExpiryDate, signedJWT), verifiableCredential); + return new JWTVerificationResult(validateSignature(withCredentialsValidation, signedJWT, didResolver) + && validateJWTExpiryDate(withCredentialExpiryDate, signedJWT) + && !checkRevocationStatus(token, verifiableCredential, new HashMap<>()) + , verifiableCredential); } @@ -310,10 +329,11 @@ private boolean validateJWTExpiryDate(boolean withExpiryDate, SignedJWT signedJW * * @param verificationRequest the verification request * @param withCredentialExpiryDate the with credential expiry date + * @param token the token * @return the map */ - public Map credentialsValidation(CredentialVerificationRequest verificationRequest, boolean withCredentialExpiryDate) { - return credentialsValidation(verificationRequest, true, withCredentialExpiryDate); + public Map credentialsValidation(CredentialVerificationRequest verificationRequest, boolean withCredentialExpiryDate, String token) { + return credentialsValidation(verificationRequest, true, withCredentialExpiryDate, token); } /** @@ -322,10 +342,12 @@ public Map credentialsValidation(CredentialVerificationRequest v * @param verificationRequest the verification request * @param withCredentialsValidation the with credentials validation * @param withCredentialExpiryDate the with credential expiry date + * @param token the token * @return the map */ @SneakyThrows - public Map credentialsValidation(CredentialVerificationRequest verificationRequest, boolean withCredentialsValidation, boolean withCredentialExpiryDate) { + public Map credentialsValidation(CredentialVerificationRequest verificationRequest, boolean withCredentialsValidation, + boolean withCredentialExpiryDate, String token) { HttpClient httpClient = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) .build(); @@ -337,31 +359,41 @@ public Map credentialsValidation(CredentialVerificationRequest v VerifiableCredential verifiableCredential; boolean dateValidation = true; + boolean revoked = false; if (verificationRequest.containsKey(StringPool.VC_JWT_KEY)) { - JWTVerificationResult result = verifyVCAsJWT((String) verificationRequest.get(StringPool.VC_JWT_KEY), didResolver, withCredentialsValidation, withCredentialExpiryDate); + JWTVerificationResult result = verifyVCAsJWT((String) verificationRequest.get(StringPool.VC_JWT_KEY), didResolver, withCredentialsValidation, withCredentialExpiryDate, token); valid = result.valid; } else { - verifiableCredential = new VerifiableCredential(verificationRequest); LinkedDataProofValidation proofValidation = LinkedDataProofValidation.newInstance(didResolver); - - if (withCredentialsValidation) { valid = proofValidation.verify(verifiableCredential); } else { valid = true; } + revoked = checkRevocationStatus(token, verifiableCredential, response); + dateValidation = CommonService.validateExpiry(withCredentialExpiryDate, verifiableCredential, response); } - response.put(StringPool.VALID, valid && dateValidation); + response.put(StringPool.VALID, valid && dateValidation && !revoked); response.put(StringPool.VC, verificationRequest); return response; } + private boolean checkRevocationStatus(String token, VerifiableCredential verifiableCredential, Map response) { + //check revocation + if (verifiableCredential.getVerifiableCredentialStatus() != null) { + CredentialStatus credentialStatus = revocationService.checkRevocation(verifiableCredential, token); + response.put(StringPool.CREDENTIAL_STATUS, credentialStatus.getName()); + return !Objects.equals(credentialStatus.getName(), CredentialStatus.ACTIVE.getName()); + } + return false; + } + private void validateAccess(String callerBpn, Wallet issuerWallet) { //validate BPN access, VC must be issued by base wallet diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java index 084e06b1..5126108e 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/JwtPresentationES256KService.java @@ -35,9 +35,9 @@ import com.nimbusds.jwt.SignedJWT; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.exception.SignatureFailureException; import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedAlgorithmException; import org.eclipse.tractusx.ssi.lib.model.JsonLdObject; @@ -141,13 +141,13 @@ public DidDocument buildDidDocument(String bpn, Did did, List tokenServiceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.SECURITY_TOKEN_SERVICE, Verifiable.TYPE, StringPool.SECURITY_TOKEN_SERVICE, - StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host() + "/api/token"); + Map tokenServiceData = Map.of(Verifiable.ID, did.toUri() + "#" + StringPool.SECURITY_TOKEN_SERVICE, Verifiable.TYPE, StringPool.SECURITY_TOKEN_SERVICE, + StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host() + "/api/token"); org.eclipse.tractusx.ssi.lib.model.did.Service tokenService = new org.eclipse.tractusx.ssi.lib.model.did.Service(tokenServiceData); - Map credentialServiceData = Map.of(Verifiable.ID, did.toUri()+"#"+StringPool.CREDENTIAL_SERVICE, Verifiable.TYPE, StringPool.CREDENTIAL_SERVICE, - StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host()); + Map credentialServiceData = Map.of(Verifiable.ID, did.toUri() + "#" + StringPool.CREDENTIAL_SERVICE, Verifiable.TYPE, StringPool.CREDENTIAL_SERVICE, + StringPool.SERVICE_ENDPOINT, StringPool.HTTPS_SCHEME + miwSettings.host()); org.eclipse.tractusx.ssi.lib.model.did.Service credentialService = new org.eclipse.tractusx.ssi.lib.model.did.Service(credentialServiceData); - didDocument.put(StringPool.SERVICE, List.of(tokenService,credentialService)); + didDocument.put(StringPool.SERVICE, List.of(tokenService, credentialService)); didDocument = DidDocument.fromJson(didDocument.toJson()); log.debug("did document created for bpn ->{}", StringEscapeUtils.escapeJava(bpn)); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java index a121b84a..57e7d41f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java @@ -30,9 +30,11 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.JtiRecord; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -41,12 +43,10 @@ import org.eclipse.tractusx.managedidentitywallets.domain.PresentationCreationConfig; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; import org.eclipse.tractusx.managedidentitywallets.domain.VerifiableEncoding; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.exception.MissingVcTypesException; import org.eclipse.tractusx.managedidentitywallets.exception.PermissionViolationException; import org.eclipse.tractusx.managedidentitywallets.signing.SignerResult; import org.eclipse.tractusx.managedidentitywallets.signing.SigningService; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; import org.eclipse.tractusx.ssi.lib.exception.json.InvalidJsonLdException; import org.eclipse.tractusx.ssi.lib.exception.proof.JwtExpiredException; @@ -70,10 +70,6 @@ import java.util.Objects; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.BLANK_SEPARATOR; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COMA_SEPARATOR; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.UNDERSCORE; import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getClaimsSet; import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getScope; import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getStringClaim; @@ -298,10 +294,10 @@ public Map createVpWithRequiredScopes(SignedJWT innerJWT, boolea List verifiableCredentials = new ArrayList<>(); String scopeValue = getScope(jwtClaimsSet); - String[] scopes = scopeValue.split(BLANK_SEPARATOR); + String[] scopes = scopeValue.split(StringPool.BLANK_SEPARATOR); for (String scope : scopes) { - String[] scopeParts = scope.split(COLON_SEPARATOR); + String[] scopeParts = scope.split(StringPool.COLON_SEPARATOR); String vcType = scopeParts[1]; checkReadPermission(scopeParts[2]); String vcTypeNoVersion = removeVersion(vcType); @@ -357,12 +353,12 @@ private void checkReadPermission(String permission) { private void checkMissingVcs(List missingVCTypes) { if (!missingVCTypes.isEmpty()) { throw new MissingVcTypesException(String.format("Missing VC types: %s", - String.join(COMA_SEPARATOR, missingVCTypes))); + String.join(StringPool.COMA_SEPARATOR, missingVCTypes))); } } private String removeVersion(String vcType) { - String[] parts = vcType.split(UNDERSCORE); + String[] parts = vcType.split(StringPool.UNDERSCORE); return (parts.length > 1) ? parts[0] : vcType; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java index af991729..d9e67078 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java @@ -26,9 +26,9 @@ import com.nimbusds.jwt.SignedJWT; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.utils.CustomSignedJWTVerifier; import org.eclipse.tractusx.managedidentitywallets.utils.TokenValidationUtils; import org.springframework.stereotype.Service; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java index ab7a636a..548a8bfe 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java @@ -27,7 +27,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.bouncycastle.util.io.pem.PemReader; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletKeyRepository; import org.eclipse.tractusx.managedidentitywallets.exception.UnsupportedAlgorithmException; @@ -97,6 +97,7 @@ public Object getPrivateKeyByKeyId(String keyId, SupportedAlgorithms supportedAl return privateKey; } } + /** * Gets private key by wallet identifier. * @@ -126,7 +127,7 @@ private static Object getKeyObject(SupportedAlgorithms algorithm, String private /** * Gets wallet key by wallet id. * - * @param walletId the wallet id + * @param walletId the wallet id * @param supportedAlgorithms the algorithm of private key * @return the wallet key by wallet identifier */ diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java index 0ca388ac..e3a835b3 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java @@ -37,9 +37,12 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; @@ -48,13 +51,10 @@ import org.eclipse.tractusx.managedidentitywallets.domain.KeyCreationConfig; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.exception.DuplicateWalletProblem; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.signing.SigningService; import org.eclipse.tractusx.managedidentitywallets.utils.CommonUtils; import org.eclipse.tractusx.managedidentitywallets.utils.EncryptionUtils; -import org.eclipse.tractusx.managedidentitywallets.utils.Validate; import org.eclipse.tractusx.ssi.lib.crypt.KeyPair; import org.eclipse.tractusx.ssi.lib.crypt.jwk.JsonWebKey; import org.eclipse.tractusx.ssi.lib.did.web.DidWebFactory; @@ -78,10 +78,6 @@ import java.util.Map; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.REFERENCE_KEY; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.VAULT_ACCESS_TOKEN; - /** * The type Wallet service. */ @@ -105,8 +101,6 @@ public class WalletService extends BaseService { private final SpecificationUtil walletSpecificationUtil; - private final IssuersCredentialService issuersCredentialService; - private final CommonService commonService; private final Map availableSigningServices; @@ -302,8 +296,8 @@ private Wallet createWallet(CreateWalletRequest request, boolean authority, Stri WalletKey.builder() .wallet(wallet) .keyId(e.keyId) - .referenceKey(REFERENCE_KEY) - .vaultAccessToken(VAULT_ACCESS_TOKEN) + .referenceKey(StringPool.REFERENCE_KEY) + .vaultAccessToken(StringPool.VAULT_ACCESS_TOKEN) .privateKey(encryptionUtils.encrypt(CommonUtils.getKeyString(e.keyPair.getPrivateKey().asByte(), StringPool.PRIVATE_KEY))) .publicKey(encryptionUtils.encrypt(CommonUtils.getKeyString(e.keyPair.getPublicKey().asByte(), StringPool.PUBLIC_KEY))) .algorithm(e.algorithm.name()) @@ -319,13 +313,13 @@ private Wallet createWallet(CreateWalletRequest request, boolean authority, Stri } private Did createDidJson(String didUrl) { - String[] split = didUrl.split(COLON_SEPARATOR); + String[] split = didUrl.split(StringPool.COLON_SEPARATOR); if (split.length == 1) { return DidWebFactory.fromHostname(didUrl); } else if (split.length == 2) { return DidWebFactory.fromHostnameAndPath(split[0], split[1]); } else { - int i = didUrl.lastIndexOf(COLON_SEPARATOR); + int i = didUrl.lastIndexOf(StringPool.COLON_SEPARATOR); String[] splitByLast = { didUrl.substring(0, i), didUrl.substring(i + 1) }; return DidWebFactory.fromHostnameAndPath(splitByLast[0], splitByLast[1]); } @@ -345,7 +339,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { CreateWalletRequest request = CreateWalletRequest.builder() .companyName(miwSettings.authorityWalletName()) .businessPartnerNumber(miwSettings.authorityWalletBpn()) - .didUrl(miwSettings.host() + COLON_SEPARATOR + miwSettings.authorityWalletBpn()) + .didUrl(miwSettings.host() + StringPool.COLON_SEPARATOR + miwSettings.authorityWalletBpn()) .build(); wallets[0] = createWallet(request, true, miwSettings.authorityWalletBpn()); log.info("Authority wallet created with bpn {}", StringEscapeUtils.escapeJava(miwSettings.authorityWalletBpn())); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/revocation/RevocationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/revocation/RevocationService.java new file mode 100644 index 00000000..bd808834 --- /dev/null +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/revocation/RevocationService.java @@ -0,0 +1,125 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.service.revocation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.SignedJWT; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.RevocationPurpose; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; +import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; +import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; +import org.eclipse.tractusx.managedidentitywallets.dto.StatusListRequest; +import org.eclipse.tractusx.managedidentitywallets.exception.RevocationException; +import org.eclipse.tractusx.managedidentitywallets.revocation.RevocationClient; +import org.eclipse.tractusx.managedidentitywallets.service.CommonService; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatusList2021Entry; +import org.eclipse.tractusx.ssi.lib.serialization.SerializeUtil; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * The `RevocationService` class is a Java service that handles the revocation of credentials and + * the creation of status lists. + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class RevocationService { + + private final RevocationClient revocationClient; + + private final CommonService commonService; + + private final ObjectMapper objectMapper; + + + @SneakyThrows + public void revokeCredential(CredentialVerificationRequest verificationRequest, String callerBpn, String token) { + VerifiableCredential verifiableCredential; + if (verificationRequest.containsKey(StringPool.VC_JWT_KEY)) { + SignedJWT signedJWT = SignedJWT.parse((String) verificationRequest.get(StringPool.VC_JWT_KEY)); + Map claims = objectMapper.readValue(signedJWT.getPayload().toBytes(), Map.class); + String vcClaim = objectMapper.writeValueAsString(claims.get("vc")); + Map map = SerializeUtil.fromJson(vcClaim); + verifiableCredential = new VerifiableCredential(map); + } else { + verifiableCredential = new VerifiableCredential(verificationRequest); + } + //check if credential status is not null + Validate.isTrue(verifiableCredential.getVerifiableCredentialStatus().isEmpty()).launch(new BadDataException("Provided credential is not revocable")); + + //Fetch issuer waller + Wallet issuerWallet = commonService.getWalletByIdentifier(verifiableCredential.getIssuer().toString()); + + //check caller must be issuer of VC + Validate.isFalse(issuerWallet.getBpn().equals(callerBpn)).launch(new ForbiddenException("Invalid credential access")); + + //check if VC is already Revoked + CredentialStatus credentialStatus = checkRevocation(verifiableCredential, token); + + if (credentialStatus.getName().equals(CredentialStatus.REVOKED.getName())) { + throw new RevocationException(HttpStatus.CONFLICT.value(), "Credential is already revoked", Map.of("message", "Credential is already revoked")); + } + revocationClient.revokeCredential(verifiableCredential.getVerifiableCredentialStatus(), token); + log.info("Credential with id {} is revoked by caller bpn {}", verifiableCredential.getId(), callerBpn); + } + + /** + * Gets status list entry. + * + * @param issuerId the issuer id + * @param token the token + * @return the status list entry + */ + public VerifiableCredentialStatusList2021Entry getStatusListEntry(@NotNull String issuerId, String token) { + StatusListRequest statusListRequest = StatusListRequest.builder() + .issuerId(issuerId) + .purpose(RevocationPurpose.REVOCATION.name().toLowerCase()) + .build(); + + return new VerifiableCredentialStatusList2021Entry(revocationClient.getStatusListEntry(statusListRequest, token)); + } + + /** + * Check revocation credential status. + * + * @param verifiableCredential the verifiable credential + * @param token the token + * @return the credential status + */ + public CredentialStatus checkRevocation(@NotNull VerifiableCredential verifiableCredential, String token) { + Map response = revocationClient.verifyCredentialStatus(verifiableCredential.getVerifiableCredentialStatus(), token); + log.debug("Revocation status for VC id->{} -> {}", verifiableCredential.getId(), response.get("status")); + return CredentialStatus.valueOf(response.get("status").toUpperCase()); + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/KeyProvider.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/KeyProvider.java index e8b925da..a2b45c27 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/KeyProvider.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/KeyProvider.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.signing; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; import org.eclipse.tractusx.managedidentitywallets.domain.DID; import org.eclipse.tractusx.managedidentitywallets.domain.KeyPair; @@ -39,7 +39,6 @@ public interface KeyProvider { /** * @param keyName the name of the key that is to be retrieved * @return the key as a byte-array - * */ Object getPrivateKey(String keyName, SupportedAlgorithms algorithm); @@ -54,11 +53,11 @@ public interface KeyProvider { /** * @return the type of KeyProvider - * * @see KeyStorageType */ KeyStorageType getKeyStorageType(); KeyPair getKeyPair(DID self); + KeyPair getKeyPair(String bpn); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalKeyProvider.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalKeyProvider.java index 93353361..b1c39b19 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalKeyProvider.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalKeyProvider.java @@ -22,7 +22,7 @@ package org.eclipse.tractusx.managedidentitywallets.signing; import lombok.RequiredArgsConstructor; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletKeyRepository; import org.eclipse.tractusx.managedidentitywallets.domain.DID; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java index 1b1198ff..d3f6ae05 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java @@ -32,8 +32,10 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.lang3.NotImplementedException; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; import org.eclipse.tractusx.managedidentitywallets.domain.BusinessPartnerNumber; import org.eclipse.tractusx.managedidentitywallets.domain.CredentialCreationConfig; @@ -42,7 +44,6 @@ import org.eclipse.tractusx.managedidentitywallets.domain.PresentationCreationConfig; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; import org.eclipse.tractusx.managedidentitywallets.domain.VerifiableEncoding; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenService; import org.eclipse.tractusx.managedidentitywallets.service.JwtPresentationES256KService; import org.eclipse.tractusx.ssi.lib.crypt.IKeyGenerator; @@ -101,6 +102,8 @@ public class LocalSigningServiceImpl implements LocalSigningService { // Autowired by name!!! private final SecureTokenService localSecureTokenService; + private final RevocationSettings revocationSettings; + @Override public SignerResult createCredential(CredentialCreationConfig config) { @@ -112,6 +115,9 @@ public SignerResult createCredential(CredentialCreationConfig config) { return resultBuilder.jsonLd(createVerifiableCredential(config, privateKeyBytes)).build(); } case JWT -> { + + //TODO maybe this we want, currently in VC as JET, we are putting signed VC(VC with proof) as a JWT claim + //instead of this we should put VC without proof and utilize JWT signature as a proof SignedJWT verifiableCredentialAsJwt = createVerifiableCredentialAsJwt(config); return resultBuilder.jwt(verifiableCredentialAsJwt.serialize()).build(); } @@ -295,15 +301,12 @@ private VerifiablePresentation generateJsonLdPresentation(PresentationCreationCo @SneakyThrows private static VerifiableCredential createVerifiableCredential(CredentialCreationConfig config, byte[] privateKeyBytes) { - //VC Builder - // if the credential does not contain the JWS proof-context add it URI jwsUri = URI.create("https://w3id.org/security/suites/jws-2020/v1"); if (!config.getContexts().contains(jwsUri)) { config.getContexts().add(jwsUri); } - URI id = URI.create(UUID.randomUUID().toString()); VerifiableCredentialBuilder builder = new VerifiableCredentialBuilder() .context(config.getContexts()) @@ -314,6 +317,10 @@ private static VerifiableCredential createVerifiableCredential(CredentialCreatio .issuanceDate(Instant.now()) .credentialSubject(config.getSubject()); + //set status list + if (config.isRevocable()) { + builder.verifiableCredentialStatus(config.getVerifiableCredentialStatus()); + } LinkedDataProofGenerator generator = LinkedDataProofGenerator.newInstance(SignatureType.JWS); URI verificationMethod = config.getIssuerDoc().getVerificationMethods().get(0).getId(); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java index b701cf3b..9c262a85 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidator.java @@ -21,12 +21,12 @@ package org.eclipse.tractusx.managedidentitywallets.utils; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.jwt.Jwt; import java.util.Map; import java.util.TreeMap; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java index c38a50d6..b3514e7c 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CommonUtils.java @@ -26,11 +26,12 @@ import lombok.experimental.UtilityClass; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.domain.CredentialCreationConfig; import org.eclipse.tractusx.managedidentitywallets.dto.SecureTokenRequest; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialType; import org.springframework.util.MultiValueMap; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java index 903faceb..4df8e3a1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/CustomSignedJWTVerifier.java @@ -31,7 +31,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.service.DidDocumentService; import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; import org.eclipse.tractusx.ssi.lib.exception.proof.UnsupportedVerificationMethodException; @@ -52,7 +52,7 @@ public class CustomSignedJWTVerifier { private final DidDocumentService didDocumentService; public static final String KID = "kid"; - @SneakyThrows({UnsupportedVerificationMethodException.class}) + @SneakyThrows({ UnsupportedVerificationMethodException.class }) public boolean verify(String did, SignedJWT jwt) throws JOSEException { VerificationMethod verificationMethod = checkVerificationMethod(did, jwt); if (JWKVerificationMethod.isInstance(verificationMethod)) { diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java index 7e6d3cad..1a2eca48 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java @@ -25,8 +25,8 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import lombok.experimental.UtilityClass; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @@ -36,10 +36,10 @@ import java.util.Optional; import java.util.TreeMap; -import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; +import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; +import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; /** * The type Token parsing utils. diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java index cea94cb5..3c58b839 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtils.java @@ -23,7 +23,7 @@ import com.nimbusds.jwt.JWTClaimsSet; import lombok.RequiredArgsConstructor; -import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors; import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; import org.eclipse.tractusx.managedidentitywallets.service.DidDocumentService; import org.springframework.stereotype.Component; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/Validate.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/Validate.java deleted file mode 100644 index a33fb97c..00000000 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/Validate.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ****************************************************************************** - */ - -package org.eclipse.tractusx.managedidentitywallets.utils; - -import java.util.Objects; - -/** - * The type Validate. - * - * @param the type parameter - */ -public class Validate { - private T value; - private boolean match = false; - - private Validate() { - } - - private Validate(T value) { - this.value = value; - } - - /** - * Value validate. - * - * @param the type parameter - * @param value the value - * @return the validate - */ - public static Validate value(V value) { - return new Validate<>(value); - } - - /** - * Is true validate. - * - * @param the type parameter - * @param condition the condition - * @return the validate - */ - public static Validate isTrue(boolean condition) { - Validate validate = new Validate<>(); - if (condition) { - validate.match = true; - } - return validate; - } - - /** - * Throws if {@code condition} is false - * - * @param the type parameter - * @param condition the condition - * @return validate validate - */ - public static Validate isFalse(boolean condition) { - Validate validate = new Validate<>(); - if (!condition) { - validate.match = true; - } - return validate; - } - - /** - * Is null validate. - * - * @param the type parameter - * @param value the value - * @return the validate - */ - public static Validate isNull(T value) { - return new Validate<>(value).isNull(); - } - - /** - * Is not null validate. - * - * @param the type parameter - * @param value the value - * @return the validate - */ - public static Validate isNotNull(T value) { - return new Validate<>(value).isNotNull(); - } - - - /** - * Is not empty validate. - * - * @return the validate - */ - public Validate isNotEmpty() { - if (match || Objects.isNull(value) || String.valueOf(value).trim().isEmpty()) { - match = true; - } - return this; - } - - /** - * Is null validate. - * - * @return the validate - */ - public Validate isNull() { - if (match || Objects.isNull(value)) { - match = true; - } - return this; - } - - /** - * Is not null validate. - * - * @return the validate - */ - public Validate isNotNull() { - if (match || !Objects.isNull(value)) { - match = true; - } - return this; - } - - /** - * Throw passed exception if expression is match - * - * @param e exception to throw - * @return the t - */ - public T launch(RuntimeException e) { - if (match) { - throw e; - } - return value; - } -} \ No newline at end of file diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java index 1f6dd6e4..5ecd58f0 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/validator/SecureTokenRequestValidator.java @@ -33,6 +33,8 @@ public class SecureTokenRequestValidator implements Validator { public static final String LINKED_MULTI_VALUE_MAP_CLASS_NAME = "org.springframework.util.LinkedMultiValueMap"; + public static final String ACCESS_TOKEN = "accessToken"; + public static final String BEARER_ACCESS_SCOPE = "bearerAccessScope"; @Override public boolean supports(Class clazz) { @@ -53,18 +55,18 @@ public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "grantType", "grant_type.empty", "The 'grant_type' cannot be empty or missing."); if (secureTokenRequest.getAccessToken() != null && secureTokenRequest.getBearerAccessScope() != null) { - errorsHandled.rejectValue("accessToken", "access_token.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); - errorsHandled.rejectValue("bearerAccessScope", "bearer_access_scope.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); + errorsHandled.rejectValue(ACCESS_TOKEN, "access_token.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); + errorsHandled.rejectValue(BEARER_ACCESS_SCOPE, "bearer_access_scope.incompatible", "The 'access_token' and the 'bearer_access_token' cannot be set together."); } if (secureTokenRequest.getAccessToken() == null && secureTokenRequest.getBearerAccessScope() == null) { - errorsHandled.rejectValue("accessToken", "access_token.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); - errorsHandled.rejectValue("bearerAccessScope", "bearer_access_scope.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); + errorsHandled.rejectValue(ACCESS_TOKEN, "access_token.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); + errorsHandled.rejectValue(BEARER_ACCESS_SCOPE, "bearer_access_scope.incompatible", "Both the 'access_token' and the 'bearer_access_scope' are missing. At least one must be set."); } if (secureTokenRequest.getAccessToken() != null) { - ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "accessToken", "access_token.empty", "The 'access_token' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, ACCESS_TOKEN, "access_token.empty", "The 'access_token' cannot be empty or missing."); } if (secureTokenRequest.getBearerAccessScope() != null) { - ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, "bearerAccessScope", "bearer_access_scope.empty", "The 'bearer_access_scope' cannot be empty or missing."); + ValidationUtils.rejectIfEmptyOrWhitespace(errorsHandled, BEARER_ACCESS_SCOPE, "bearer_access_scope.empty", "The 'bearer_access_scope' cannot be empty or missing."); } } } diff --git a/miw/src/main/resources/application.yaml b/miw/src/main/resources/application.yaml index aa638730..d705efe1 100644 --- a/miw/src/main/resources/application.yaml +++ b/miw/src/main/resources/application.yaml @@ -18,7 +18,7 @@ ################################################################################ server: - port: ${APPLICATION_PORT:8087} + port: ${APPLICATION_PORT:8080} shutdown: graceful compression: enabled: true @@ -30,9 +30,9 @@ spring: application: name: miw datasource: - url: jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/${DB_NAME}?useSSL=${USE_SSL} - username: ${DB_USER_NAME} - password: ${DB_PASSWORD} + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5434}/${DB_NAME:miw}?useSSL=${USE_SSL:false} + username: ${DB_USER_NAME:admin} + password: ${DB_PASSWORD:admin} initialization-mode: always hikari: maximumPoolSize: ${DB_POOL_SIZE:10} @@ -89,11 +89,11 @@ logging: managedidentitywallets: ${APP_LOG_LEVEL:INFO} miw: - host: ${MIW_HOST_NAME:localhost} - encryptionKey: ${ENCRYPTION_KEY} + host: ${MIW_HOST_NAME:a888-203-129-213-107.ngrok-free.app} + encryptionKey: ${ENCRYPTION_KEY:PQL+9tK5jaKXR+9hepTsNg==} authorityWalletBpn: ${AUTHORITY_WALLET_BPN:BPNL000000000000} authorityWalletName: ${AUTHORITY_WALLET_NAME:Catena-X} - authorityWalletDid: ${AUTHORITY_WALLET_DID:did:web:localhost:BPNL000000000000} + authorityWalletDid: ${AUTHORITY_WALLET_DID:did:web:a888-203-129-213-107.ngrok-free.app:BPNL000000000000} authoritySigningServiceType: ${AUTHORITY_SIGNING_SERVICE_TYPE:LOCAL} localSigningKeyStorageType: ${LOCAL_SIGNING_KEY_STORAGE_TYPE:DB} vcContexts: ${VC_SCHEMA_LINK:https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json} @@ -103,11 +103,13 @@ miw: security: enabled: true realm: ${KEYCLOAK_REALM:miw_test} - clientId: ${KEYCLOAK_CLIENT_ID} - auth-server-url: ${AUTH_SERVER_URL:http://localhost:8081} + clientId: ${KEYCLOAK_CLIENT_ID:miw_private_client} + auth-server-url: ${AUTH_SERVER_URL:http://localhost:28080} auth-url: ${miw.security.auth-server-url}/realms/${miw.security.realm}/protocol/openid-connect/auth token-url: ${miw.security.auth-server-url}/realms/${miw.security.realm}/protocol/openid-connect/token refresh-token-url: ${miw.security.token-url} + revocation: + url: ${REVOCATION_SERVICE_URL:http://localhost:8081} sts: diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java index d85994f1..a181c2e1 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationIatpFilterTest.java @@ -22,9 +22,9 @@ package org.eclipse.tractusx.managedidentitywallets.controller; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; import org.junit.jupiter.api.Assertions; @@ -47,8 +47,8 @@ import java.util.List; import java.util.Map; -import static org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors.NONCE_MISSING; -import static org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors.TOKEN_ALREADY_EXPIRED; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors.NONCE_MISSING; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors.TOKEN_ALREADY_EXPIRED; @DirtiesContext @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java index 58dc1165..bcf1e431 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/controller/SecureTokenControllerTest.java @@ -45,7 +45,7 @@ import java.util.List; import java.util.Map; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) @@ -104,8 +104,8 @@ void tokenJSON() { new ParameterizedTypeReference<>() { } ); - Assertions.assertEquals(response.getStatusCode(), HttpStatus.OK); - Assertions.assertEquals(response.getHeaders().getContentType(), MediaType.APPLICATION_JSON); + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); + Assertions.assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); Assertions.assertNotNull(response.getBody()); Assertions.assertNotNull(response.getBody().getOrDefault("access_token", null)); Assertions.assertNotNull(response.getBody().getOrDefault("expiresAt", null)); @@ -127,7 +127,7 @@ void tokenFormUrlencoded() { new ParameterizedTypeReference<>() { } ); - Assertions.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); Assertions.assertEquals(response.getHeaders().getContentType(), MediaType.APPLICATION_JSON); Assertions.assertNotNull(response.getBody()); Assertions.assertNotNull(response.getBody().getOrDefault("access_token", null)); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/did/DidDocumentsTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/did/DidDocumentsTest.java index 4e902df7..5645132f 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/did/DidDocumentsTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/did/DidDocumentsTest.java @@ -38,10 +38,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = {ManagedIdentityWalletsApplication.class}) -@ContextConfiguration(initializers = {TestContextInitializer.class}) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) +@ContextConfiguration(initializers = { TestContextInitializer.class }) class DidDocumentsTest { @Autowired diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfigTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfigTest.java index 07ccba5a..10da4a2e 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfigTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/domain/CredentialCreationConfigTest.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.domain; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.ssi.lib.model.did.Did; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatus; @@ -115,7 +115,7 @@ void shouldThrowWhenSettingIllegalVcId() { } @Test - void shouldNotThrowWhenVcIdValid(){ + void shouldNotThrowWhenVcIdValid() { CredentialCreationConfig.CredentialCreationConfigBuilder builder = CredentialCreationConfig.builder(); assertDoesNotThrow(() -> builder.vcId("https://test.com")); assertDoesNotThrow(() -> builder.vcId(URI.create("https://test.com"))); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java index 6be2a0c3..4c52d70c 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java @@ -28,9 +28,10 @@ import com.smartsensesolutions.java.commons.specification.SpecificationUtil; import lombok.SneakyThrows; import org.apache.commons.lang3.time.DateUtils; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.IssuersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -42,6 +43,7 @@ import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.interfaces.SecureTokenService; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.signing.LocalKeyProvider; import org.eclipse.tractusx.managedidentitywallets.signing.LocalSigningServiceImpl; import org.eclipse.tractusx.managedidentitywallets.signing.SigningService; @@ -119,6 +121,10 @@ class IssuersCredentialServiceTest { private static EncryptionUtils encryptionUtils; + private static RevocationService revocationService; + + private static RevocationSettings revocationSettings; + private static final ObjectMapper objectMapper = new ObjectMapper(); @BeforeAll @@ -131,6 +137,8 @@ public static void beforeAll() throws SQLException { issuersCredentialRepository = mock(IssuersCredentialRepository.class); secureTokenService = mock(SecureTokenService.class); walletKeyRepository = mock(WalletKeyRepository.class); + revocationService = mock(RevocationService.class); + revocationSettings = mock(RevocationSettings.class); Connection connection = mock(Connection.class); @@ -145,7 +153,7 @@ public static void beforeAll() throws SQLException { issuersCredentialRepository, miwSettings, new SpecificationUtil(), - holdersCredentialRepository, commonService, objectMapper); + holdersCredentialRepository, commonService, objectMapper, revocationService); } @BeforeEach @@ -208,7 +216,7 @@ public HoldersCredential answer(InvocationOnMock invocation) { when(walletKeyService.getPrivateKeyByKeyId(anyString(), any())).thenReturn(keyPair.getPrivateKey()); when(walletKeyRepository.getByAlgorithmAndWallet_Bpn(anyString(), anyString())).thenReturn(walletKey); - LocalSigningServiceImpl localSigningService = new LocalSigningServiceImpl(secureTokenService); + LocalSigningServiceImpl localSigningService = new LocalSigningServiceImpl(secureTokenService, revocationSettings); localSigningService.setKeyProvider(new LocalKeyProvider(walletKeyService, walletKeyRepository, encryptionUtils)); Map map = new HashMap<>(); @@ -220,8 +228,8 @@ public HoldersCredential answer(InvocationOnMock invocation) { () -> issuersCredentialService.issueCredentialUsingBaseWallet( holderWalletBpn, verifiableCredential, - true, - baseWalletBpn)); + true, false, + baseWalletBpn, "dummy token")); validateCredentialResponse(credentialsResponse, MockUtil.buildDidDocument(new Did(new DidMethod("web"), new DidMethodIdentifier("basewallet"), @@ -274,7 +282,7 @@ void shouldValidateAsJWT() throws DidParseException { credentialVerificationRequest.setJwt(serialized); Map stringObjectMap = assertDoesNotThrow( - () -> issuersCredentialService.credentialsValidation(credentialVerificationRequest, true)); + () -> issuersCredentialService.credentialsValidation(credentialVerificationRequest, true, "dummy token")); assertTrue((Boolean) stringObjectMap.get(StringPool.VALID)); } } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java index 4614ea02..d357b96f 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationServiceTest.java @@ -27,9 +27,9 @@ import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; import com.nimbusds.jwt.JWTClaimsSet; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; -import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java index 1f4be3d3..6f2a31ce 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java @@ -21,8 +21,8 @@ package org.eclipse.tractusx.managedidentitywallets.utils; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.jetbrains.annotations.NotNull; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 005efcea..5e9a7e34 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -33,14 +33,14 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import lombok.SneakyThrows; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.verifiable.Verifiable; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; @@ -160,8 +160,6 @@ public static Wallet getWalletFromString(String body) throws JsonProcessingExcep } - - @NotNull public static List getVerifiableCredentials(ResponseEntity response, ObjectMapper objectMapper) throws JsonProcessingException { Map map = objectMapper.readValue(response.getBody(), Map.class); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtilsTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtilsTest.java index 6f9c0091..ef5c52c2 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtilsTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenValidationUtilsTest.java @@ -23,7 +23,7 @@ import com.nimbusds.jwt.JWTClaimsSet; import lombok.SneakyThrows; -import org.eclipse.tractusx.managedidentitywallets.constant.TokenValidationErrors; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors; import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; import org.eclipse.tractusx.managedidentitywallets.service.DidDocumentService; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java index 7da209a7..fa74e9c0 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java @@ -25,10 +25,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.controller.IssuersCredentialController; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -79,8 +79,6 @@ import java.util.Objects; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) @ExtendWith(MockitoExtension.class) @@ -252,7 +250,7 @@ void validateCredentialsWithInvalidVC() throws com.fasterxml.jackson.core.JsonPr }).thenReturn(mock); Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(false); - Map stringObjectMap = credentialController.credentialsValidation(request, false).getBody(); + Map stringObjectMap = credentialController.credentialsValidation(request, false, "dummy token").getBody(); Assertions.assertFalse(Boolean.parseBoolean(stringObjectMap.get(StringPool.VALID).toString())); } } @@ -275,7 +273,7 @@ void validateCredentialsWithExpiryCheckTrue() { }).thenReturn(mock); Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); - Map stringObjectMap = credentialController.credentialsValidation(request, true).getBody(); + Map stringObjectMap = credentialController.credentialsValidation(request, true, "dummy token").getBody(); Assertions.assertTrue(Boolean.parseBoolean(stringObjectMap.get(StringPool.VALID).toString())); Assertions.assertTrue(Boolean.parseBoolean(stringObjectMap.get(StringPool.VALIDATE_EXPIRY_DATE).toString())); } @@ -302,7 +300,7 @@ void validateCredentialsWithExpiryCheckFalse() throws com.fasterxml.jackson.core }).thenReturn(mock); Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); - Map stringObjectMap = credentialController.credentialsValidation(request, false).getBody(); + Map stringObjectMap = credentialController.credentialsValidation(request, false, "dummt token").getBody(); Assertions.assertTrue(Boolean.parseBoolean(stringObjectMap.get(StringPool.VALID).toString())); } } @@ -330,7 +328,7 @@ void validateExpiredCredentialsWithExpiryCheckTrue() throws com.fasterxml.jackso }).thenReturn(mock); Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); - Map stringObjectMap = credentialController.credentialsValidation(request, true).getBody(); + Map stringObjectMap = credentialController.credentialsValidation(request, true, "dummy token").getBody(); Assertions.assertFalse(Boolean.parseBoolean(stringObjectMap.get(StringPool.VALID).toString())); Assertions.assertFalse(Boolean.parseBoolean(stringObjectMap.get(StringPool.VALIDATE_EXPIRY_DATE).toString())); @@ -341,7 +339,7 @@ void validateExpiredCredentialsWithExpiryCheckTrue() throws com.fasterxml.jackso private Map issueVC() throws JsonProcessingException { String bpn = TestUtils.getRandomBpmNumber(); String baseBpn = miwSettings.authorityWalletBpn(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; ResponseEntity response = TestUtils.createWallet(bpn, "Test Wallet", restTemplate, baseBpn, defaultLocation); Wallet wallet = TestUtils.getWalletFromString(response.getBody()); VerifiableCredential verifiableCredential = TestUtils.issueCustomVCUsingBaseWallet(bpn, wallet.getDid(), miwSettings.authorityWalletDid(), "Type1", AuthenticationUtils.getValidUserHttpHeaders(miwSettings.authorityWalletBpn()), miwSettings, objectMapper, restTemplate); @@ -353,7 +351,7 @@ private Map issueVC() throws JsonProcessingException { private ResponseEntity issueVC(String bpn, String did, String type, HttpHeaders headers) throws JsonProcessingException { String baseBpn = miwSettings.authorityWalletBpn(); //save wallet - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; TestUtils.createWallet(bpn, did, restTemplate, baseBpn, defaultLocation); // Create VC without proof diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java index 7f6473bc..712dcf5c 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java @@ -25,10 +25,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.IssuersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -36,7 +37,6 @@ import org.eclipse.tractusx.managedidentitywallets.dao.repository.IssuersCredentialRepository; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; -import org.eclipse.tractusx.managedidentitywallets.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; import org.eclipse.tractusx.ssi.lib.did.web.DidWebFactory; @@ -61,7 +61,7 @@ import java.util.Map; import java.util.Objects; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) @@ -246,5 +246,4 @@ void issueCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcep } - } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/PresentationValidationTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/PresentationValidationTest.java index 4441d638..cd316f02 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/PresentationValidationTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/PresentationValidationTest.java @@ -28,10 +28,10 @@ import lombok.Setter; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; @@ -68,7 +68,7 @@ import java.util.Map; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) @@ -120,13 +120,13 @@ public void setup() throws DidParseException { Map type1 = TestUtils.getCredentialAsMap(miwSettings.authorityWalletBpn(), miwSettings.authorityWalletDid(), miwSettings.authorityWalletDid(), "Type1", miwSettings, new com.fasterxml.jackson.databind.ObjectMapper()); - CredentialsResponse rs1 = issuersCredentialService.issueCredentialUsingBaseWallet(tenantWallet.getDid(), type1, false, bpnOperator); + CredentialsResponse rs1 = issuersCredentialService.issueCredentialUsingBaseWallet(tenantWallet.getDid(), type1, false, false, bpnOperator, "dummy token"); vc_1 = new ObjectMapper().convertValue(rs1, VerifiableCredential.class); Map type2 = TestUtils.getCredentialAsMap(miwSettings.authorityWalletBpn(), miwSettings.authorityWalletDid(), miwSettings.authorityWalletDid(), "Type2", miwSettings, new com.fasterxml.jackson.databind.ObjectMapper()); - CredentialsResponse rs2 = issuersCredentialService.issueCredentialUsingBaseWallet(tenantWallet.getDid(), type2, false, bpnOperator); + CredentialsResponse rs2 = issuersCredentialService.issueCredentialUsingBaseWallet(tenantWallet.getDid(), type2, false, false, bpnOperator, "dummy token"); vc_2 = new ObjectMapper().convertValue(rs2, VerifiableCredential.class); } @@ -139,7 +139,7 @@ void testSuccessfulValidation() { @Test @SneakyThrows - public void testSuccessfulValidationForMultipleVC() { + void testSuccessfulValidationForMultipleVC() { Map creationResponse = createPresentationJwt(List.of(vc_1, vc_2), tenant_1); // get the payload of the json web token String encodedJwtPayload = ((String) creationResponse.get("vp")).split("\\.")[1]; @@ -153,7 +153,7 @@ public void testSuccessfulValidationForMultipleVC() { } @Test - public void testValidationFailureOfCredentialWitInvalidExpirationDate() { + void testValidationFailureOfCredentialWitInvalidExpirationDate() { // test is related to this old issue where the signature check still succeeded // https://github.com/eclipse-tractusx/SSI-agent-lib/issues/4 VerifiableCredential copyCredential = new VerifiableCredential(vc_1); @@ -166,7 +166,7 @@ public void testValidationFailureOfCredentialWitInvalidExpirationDate() { @Test - public void testValidationFailureOfCredentialWitInvalidExpirationDateInSecondCredential() { + void testValidationFailureOfCredentialWitInvalidExpirationDateInSecondCredential() { // test is related to this old issue where the signature check still succeeded // https://github.com/eclipse-tractusx/SSI-agent-lib/issues/4 VerifiableCredential copyCredential = new VerifiableCredential(vc_1); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/VerifiableCredentialIssuerEqualProofSignerTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/VerifiableCredentialIssuerEqualProofSignerTest.java index 4537be00..5c9c44f4 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/VerifiableCredentialIssuerEqualProofSignerTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/VerifiableCredentialIssuerEqualProofSignerTest.java @@ -23,9 +23,9 @@ import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.service.CommonService; import org.eclipse.tractusx.managedidentitywallets.service.PresentationService; @@ -53,11 +53,11 @@ import java.util.Map; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) -public class VerifiableCredentialIssuerEqualProofSignerTest { +class VerifiableCredentialIssuerEqualProofSignerTest { @Autowired private MIWSettings miwSettings; @@ -76,7 +76,7 @@ public class VerifiableCredentialIssuerEqualProofSignerTest { @SneakyThrows @Test - public void test() { + void test() { var bpn1 = "BPNL000000000FOO"; String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn1; var response1 = TestUtils.createWallet(bpn1, "did:web:localhost%3A8080:BPNL000000000FOO", restTemplate, miwSettings.authorityWalletBpn(), defaultLocation); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java index 31768e04..df45f40a 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java @@ -28,14 +28,14 @@ import com.nimbusds.jwt.SignedJWT; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.dao.entity.JtiRecord; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.JtiRepository; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.exception.MissingVcTypesException; import org.eclipse.tractusx.managedidentitywallets.exception.PermissionViolationException; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; @@ -58,7 +58,7 @@ import java.util.Map; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.BPN_CREDENTIAL_READ; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.BPN_CREDENTIAL_WRITE; import static org.eclipse.tractusx.managedidentitywallets.utils.TestConstants.DID_BPN_1; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java index f7495895..e0b8e2f2 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java @@ -27,10 +27,10 @@ import com.nimbusds.jwt.SignedJWT; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.controller.PresentationController; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -74,7 +74,7 @@ import java.util.Objects; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; +import static org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool.COLON_SEPARATOR; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java index 7095a256..61d18ee4 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/wallet/WalletTest.java @@ -25,11 +25,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.Curve; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; @@ -70,8 +70,6 @@ import java.util.Map; import java.util.Objects; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) @@ -139,7 +137,7 @@ void createWalletTestWithUserToken403() { String name = "Sample Wallet"; HttpHeaders headers = AuthenticationUtils.getValidUserHttpHeaders(bpn); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; CreateWalletRequest request = CreateWalletRequest.builder().businessPartnerNumber(bpn).companyName(name).didUrl(defaultLocation).build(); HttpEntity entity = new HttpEntity<>(request, headers); @@ -155,7 +153,7 @@ void createWalletWithInvalidBPNTest400() throws JSONException { String name = "Sample Wallet"; String baseBpn = miwSettings.authorityWalletBpn(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; ResponseEntity response = TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation); Assertions.assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatusCode().value()); } @@ -167,7 +165,7 @@ void createWalletTest201() throws JsonProcessingException, JSONException { String name = "Sample Wallet"; String baseBpn = miwSettings.authorityWalletBpn(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; ResponseEntity response = TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation); Assertions.assertEquals(HttpStatus.CREATED.value(), response.getStatusCode().value()); Wallet wallet = TestUtils.getWalletFromString(response.getBody()); @@ -180,10 +178,10 @@ void createWalletTest201() throws JsonProcessingException, JSONException { // both public keys will include the publicKeyJwk format to express the public key List curves = verificationMethods.stream().map(vm -> (LinkedHashMap) vm.get(JWKVerificationMethod.PUBLIC_KEY_JWK)) .map(lhm -> lhm.get(JWKVerificationMethod.JWK_CURVE).toString()).toList(); - List algorithms = Arrays.asList(Curve.SECP256K1.toString(),Curve.Ed25519.toString()); + List algorithms = Arrays.asList(Curve.SECP256K1.toString(), Curve.Ed25519.toString()); // both the Ed25519 and the secp256k1 curve keys must be present in the verificationMethod of a did document Assertions.assertTrue(curves.containsAll(algorithms)); - List assertionMethod = (List)wallet.getDidDocument().get(StringPool.ASSERTION_METHOD); + List assertionMethod = (List) wallet.getDidDocument().get(StringPool.ASSERTION_METHOD); // both public keys must be expressed in the assertionMethod Assertions.assertEquals(2, assertionMethod.size()); // both public keys will use the JsonWebKey2020 verification method type @@ -242,7 +240,7 @@ void storeCredentialsTest201() throws JsonProcessingException { String did = DidWebFactory.fromHostnameAndPath(miwSettings.host(), bpn).toString(); String baseBpn = miwSettings.authorityWalletBpn(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; TestUtils.createWallet(bpn, "name", restTemplate, baseBpn, defaultLocation); ResponseEntity response = storeCredential(bpn, did); @@ -320,7 +318,7 @@ void storeCredentialsWithDifferentHolder403() throws JsonProcessingException { String bpn = TestUtils.getRandomBpmNumber(); String did = DidWebFactory.fromHostnameAndPath(miwSettings.host(), bpn).toString(); String baseBpn = miwSettings.authorityWalletBpn(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; TestUtils.createWallet(bpn, "name", restTemplate, baseBpn, defaultLocation); HttpHeaders headers = AuthenticationUtils.getValidUserHttpHeaders("Some random pbn"); @@ -339,7 +337,7 @@ void createWalletWithDuplicateBpn409() throws JsonProcessingException, JSONExcep String baseBpn = miwSettings.authorityWalletBpn(); //save wallet - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; ResponseEntity response = TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation); TestUtils.getWalletFromString(response.getBody()); Assertions.assertEquals(HttpStatus.CREATED.value(), response.getStatusCode().value()); @@ -366,7 +364,7 @@ void getWalletByIdentifierWithInvalidBPNTest403() { String bpn = TestUtils.getRandomBpmNumber(); String baseBpn = miwSettings.authorityWalletBpn(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; TestUtils.createWallet(bpn, "sample name", restTemplate, baseBpn, defaultLocation); //create token with different BPN @@ -385,7 +383,7 @@ void getWalletByIdentifierBPNTest200() throws JsonProcessingException, JSONExcep String baseBpn = miwSettings.authorityWalletBpn(); //Create entry - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; Wallet wallet = TestUtils.getWalletFromString(TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation).getBody()); //get wallet without credentials @@ -410,7 +408,7 @@ void getWalletByIdentifierBPNWithCredentialsTest200() throws JsonProcessingExcep String baseBpn = miwSettings.authorityWalletBpn(); //Create entry - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; Wallet wallet = TestUtils.getWalletFromString(TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation).getBody()); //store credentials @@ -440,7 +438,7 @@ void getWalletByIdentifierDidTest200() throws JsonProcessingException, JSONExcep String baseBpn = miwSettings.authorityWalletBpn(); //Create entry - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; Wallet wallet = TestUtils.getWalletFromString(TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation).getBody()); HttpHeaders headers = AuthenticationUtils.getValidUserHttpHeaders(bpn); @@ -485,7 +483,7 @@ void getWallets200() throws JsonProcessingException, JSONException { String name = "Sample Name"; String baseBpn = miwSettings.authorityWalletBpn(); //Create entry - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; TestUtils.createWallet(bpn, name, restTemplate, baseBpn, defaultLocation); HttpHeaders headers = AuthenticationUtils.getValidUserHttpHeaders(); diff --git a/revocation-service/build.gradle b/revocation-service/build.gradle index a5f0ef39..edb64565 100644 --- a/revocation-service/build.gradle +++ b/revocation-service/build.gradle @@ -36,6 +36,10 @@ java { } dependencies { + + //project deps + implementation project(":wallet-commons") + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' @@ -46,7 +50,7 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation "org.springdoc:springdoc-openapi-starter-common:${openApiVersion}" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${openApiVersion}" - implementation "com.google.code.gson:gson:${gsonVersion}" + implementation "com.google.code.gson:gson:${gsonVersion}" implementation 'org.liquibase:liquibase-core' implementation "org.eclipse.tractusx.ssi:cx-ssi-lib:${ssiLibVersion}" compileOnly "org.projectlombok:lombok:${lombokVersion}" @@ -111,8 +115,6 @@ htmlDependencyReport { } - - jacocoTestCoverageVerification { afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java index 168e094a..13c521ee 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java @@ -36,6 +36,7 @@ public class RevocationApiControllerApiDocs { + @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @ApiResponses( @@ -43,7 +44,16 @@ public class RevocationApiControllerApiDocs { @ApiResponse( responseCode = "200", description = "Verifiable credential revoked successfully.", - content = @Content()), + content = @Content(examples = { @ExampleObject(description = "if credential is revoked", value = """ + { + "status":"revoked" + } + """), + @ExampleObject(description = "if credential is is active", value = """ + { + "status":"active" + } + """) })), @ApiResponse( responseCode = "401", description = "UnauthorizedException: invalid token", @@ -60,15 +70,68 @@ public class RevocationApiControllerApiDocs { examples = @ExampleObject( value = - "{\n" - + " \"type\": \"about:blank\",\n" - + " \"title\": \"Revocation service error\",\n" - + " \"status\": 409,\n" - + " \"detail\": \"Credential already revoked\",\n" - + " \"type\": \"BitstringStatusListEntry\",\n" - + " \"instance\": \"/api/v1/revocations/revoke\",\n" - + " \"timestamp\": 1707133388128\n" - + "}"), + ""), + mediaType = "application/json")), + @ApiResponse( + responseCode = "500", + description = "RevocationServiceException: Internal Server Error", + content = @Content()) + }) + @RequestBody( + content = { + @Content( + examples = + @ExampleObject( + value = """ + { + "id": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1#12", + "statusPurpose": "revocation", + "statusListIndex": "12", + "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusListEntry" + } + """), + mediaType = "application/json") + }) + @Operation( + summary = "Verify Revocation status", + description = "Verify revocation status of Credential") + public @interface verifyCredentialDocs { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Verifiable credential revoked successfully.", + content = @Content()), + @ApiResponse( + responseCode = "401", + description = "UnauthorizedException: invalid token", + content = @Content()), + @ApiResponse( + responseCode = "403", + description = "ForbiddenException: invalid caller", + content = @Content()), + @ApiResponse( + responseCode = "409", + description = "ConflictException: Revocation service error", + content = + @Content( + examples = + @ExampleObject( + value = """ + { + "type": "BitstringStatusListEntry", + "title": "Revocation service error", + "status": "409", + "detail": "Credential already revoked", + "instance": "/api/v1/revocations/revoke", + "timestamp": 1707133388128 + } + """), mediaType = "application/json")), @ApiResponse( responseCode = "500", @@ -80,14 +143,15 @@ public class RevocationApiControllerApiDocs { @Content( examples = @ExampleObject( - value = - "{\n" - + " \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0\",\n" - + " \"statusPurpose\": \"revocation\",\n" - + " \"statusListIndex\": \"0\",\n" - + " \"statusListCredential\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\",\n" - + " \"type\": \"BitstringStatusListEntry\"\n" - + "}"), + value = """ + { + "id": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1#12", + "statusPurpose": "revocation", + "statusListIndex": "12", + "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusListEntry" + } + """), mediaType = "application/json") }) @Operation( @@ -107,14 +171,15 @@ public class RevocationApiControllerApiDocs { @Content( examples = @ExampleObject( - value = - "{\n" - + " \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0\",\n" - + " \"statusPurpose\": \"revocation\",\n" - + " \"statusListIndex\": \"0\",\n" - + " \"statusListCredential\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\",\n" - + " \"type\": \"BitstringStatusListEntry\"\n" - + "}"), + value = """ + { + "id": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1#17", + "statusPurpose": "revocation", + "statusListIndex": "17", + "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusListEntry" + } + """), mediaType = "application/json") }), @ApiResponse( @@ -135,11 +200,12 @@ public class RevocationApiControllerApiDocs { @Content( examples = @ExampleObject( - value = - "{\n" - + " \"purpose\": \"revocation\",\n" - + " \"issuerId\": \"did:web:localhost:BPNL000000000000\"\n" - + "}"), + value = """ + { + "purpose": "revocation", + "issuerId": "did:web:localhost:BPNL000000000000" + } + """), mediaType = "application/json") }) @Operation( diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java index e237b86a..f8b22a5f 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandling.java @@ -23,8 +23,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.revocation.exception.CredentialAlreadyRevokedException; -import org.eclipse.tractusx.managedidentitywallets.revocation.exception.ForbiddenException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.annotation.ExceptionHandler; diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java index 1424ba96..ca9e786f 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfig.java @@ -23,7 +23,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.tractusx.managedidentitywallets.revocation.constant.ApplicationRole; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.ApplicationRole; import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java index b91f949e..25f040c9 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/RevocationApiEndpoints.java @@ -26,6 +26,7 @@ public class RevocationApiEndpoints { public static final String REVOCATION_API = "/api/v1/revocations"; public static final String CREDENTIALS = "/api/credentials"; public static final String REVOKE = "/revoke"; + public static final String VERIFY = "/verify"; public static final String STATUS_ENTRY = "/status-entry"; public static final String CREDENTIALS_BY_ISSUER = "/credentials"; public static final String CREDENTIALS_STATUS_INDEX = diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java index 0c982b91..341a4780 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/BaseController.java @@ -21,8 +21,8 @@ package org.eclipse.tractusx.managedidentitywallets.revocation.controllers; -import org.eclipse.tractusx.managedidentitywallets.revocation.utils.StringPool; -import org.eclipse.tractusx.managedidentitywallets.revocation.utils.Validate; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java index f4b0d82f..580ff2af 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java @@ -26,14 +26,14 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.revocation.apidocs.RevocationApiControllerApiDocs; import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; -import org.eclipse.tractusx.managedidentitywallets.revocation.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.revocation.exception.RevocationServiceException; import org.eclipse.tractusx.managedidentitywallets.revocation.services.RevocationService; -import org.eclipse.tractusx.managedidentitywallets.revocation.utils.Validate; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -48,6 +48,7 @@ import org.springframework.web.bind.annotation.RestController; import java.security.Principal; +import java.util.Map; /** * The RevocationApiController class is a REST controller that handles revocation-related API @@ -112,6 +113,21 @@ public ResponseEntity revokeCredential( return new ResponseEntity<>(HttpStatus.OK); } + + @RevocationApiControllerApiDocs.verifyCredentialDocs + @PostMapping(RevocationApiEndpoints.VERIFY) + public ResponseEntity> verifyRevocation( + @Valid @RequestBody CredentialStatusDto dto, + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token, + Principal principal) { + Validate.isFalse( + getBPNFromToken(principal).equals(revocationService.extractBpnFromURL(dto.id()))) + .launch(new ForbiddenException("Invalid caller")); + + + return ResponseEntity.ofNullable(revocationService.verifyStatus(dto)); + } + /** * The function `getCredentialsByIssuerId` retrieves a list of credentials by their issuer ID. * diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java index cf3ce0b1..e3f0bdc1 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDto.java @@ -24,7 +24,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.RevocationPurpose; import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; public record CredentialStatusDto( @@ -38,7 +38,7 @@ public record CredentialStatusDto( || Integer.parseInt(statusListIndex) > BitSetManager.BITSET_SIZE - 1) { throw new IllegalArgumentException("statusListIndex is out of range"); } - if (!statusPurpose.equalsIgnoreCase(PurposeType.REVOCATION.toString())) { + if (!statusPurpose.equalsIgnoreCase(RevocationPurpose.REVOCATION.name())) { throw new IllegalArgumentException("invalid statusPurpose"); } if (!type.equals(StatusListCredentialSubject.TYPE_ENTRY)) { diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java index 9a77d39e..8c033c24 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDto.java @@ -25,7 +25,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.RevocationPurpose; @Valid public record StatusEntryDto( @@ -34,7 +34,7 @@ public record StatusEntryDto( @NotNull @NotBlank @JsonProperty("issuerId") String issuerId) { public StatusEntryDto { - if (!purpose.equalsIgnoreCase(PurposeType.REVOCATION.toString())) { + if (!purpose.equalsIgnoreCase(RevocationPurpose.REVOCATION.name())) { throw new IllegalArgumentException("purpose should only be revocation at this time"); } } diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java index 1704295b..f0ebd35c 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java @@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.revocation.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; import org.eclipse.tractusx.managedidentitywallets.revocation.domain.BPN; @@ -38,19 +39,34 @@ import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListCredentialRepository; import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListIndexRepository; import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; +import org.eclipse.tractusx.managedidentitywallets.revocation.utils.CommonUtils; +import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; +import org.eclipse.tractusx.ssi.lib.did.web.DidWebResolver; +import org.eclipse.tractusx.ssi.lib.did.web.util.DidWebParser; +import org.eclipse.tractusx.ssi.lib.exception.did.DidParseException; +import org.eclipse.tractusx.ssi.lib.exception.json.TransformJsonLdException; +import org.eclipse.tractusx.ssi.lib.exception.key.InvalidPublicKeyFormatException; +import org.eclipse.tractusx.ssi.lib.exception.proof.NoVerificationKeyFoundException; +import org.eclipse.tractusx.ssi.lib.exception.proof.SignatureParseException; +import org.eclipse.tractusx.ssi.lib.exception.proof.SignatureVerificationFailedException; +import org.eclipse.tractusx.ssi.lib.exception.proof.UnsupportedSignatureTypeException; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialBuilder; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialType; +import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofValidation; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.net.URI; +import java.net.http.HttpClient; import java.time.Instant; import java.util.ArrayList; +import java.util.BitSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,6 +79,8 @@ @RequiredArgsConstructor public class RevocationService { + public static final String ENCODED_LIST = "encodedList"; + private final StatusListCredentialRepository statusListCredentialRepository; private final StatusListIndexRepository statusListIndexRepository; @@ -71,6 +89,52 @@ public class RevocationService { private final MIWSettings miwSettings; + @Transactional + + public Map verifyStatus(CredentialStatusDto statusDto) { + + String url = statusDto.statusListCredential(); + + String[] values = CommonUtils.extractValuesFromURL(url); + VerifiableCredential statusListCredential = getStatusListCredential(values[0], values[1], values[2]); + if (Objects.isNull(statusListCredential)) { + log.error("Status list VC not found for issuer -> {}", + values[0]); + throw new BadDataException("Status list VC not found for issuer -> " + values[0]); + } + + //validate status list VC + validateStatusListVC(statusListCredential); + + + String encodedList = statusListCredential.getCredentialSubject().get(0).get(ENCODED_LIST).toString(); + + BitSet bitSet = BitSetManager.decompress(BitSetManager.decodeFromString(encodedList)); + int index = Integer.parseInt(statusDto.statusListIndex()); + boolean status = bitSet.get(index); + return Map.of("status", status ? "revoked" : "active"); + } + + + private void validateStatusListVC(VerifiableCredential statusListCredential) { + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + DidResolver didResolver = new DidWebResolver(httpClient, new DidWebParser(), true); + LinkedDataProofValidation proofValidation = LinkedDataProofValidation.newInstance(didResolver); + boolean valid = false; + try { + valid = proofValidation.verify(statusListCredential); + } catch (UnsupportedSignatureTypeException | SignatureParseException | DidParseException | + InvalidPublicKeyFormatException | SignatureVerificationFailedException | + NoVerificationKeyFoundException | TransformJsonLdException e) { + log.error("Verification failed with error -> {}", e.getMessage(), e); + } + if (!valid) { + throw new BadDataException("Status list credential is not valid"); + } + } + /** * The `revoke` function revokes a credential by updating the status list credential with a new * subject and saving it to the repository. diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/CommonUtils.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/CommonUtils.java new file mode 100644 index 00000000..767d0c0d --- /dev/null +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/CommonUtils.java @@ -0,0 +1,53 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.utils; + +import lombok.experimental.UtilityClass; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@UtilityClass +public class CommonUtils { + + /** + * Extracts the BPN number, purpose and credential index from the URL + * + * @param url the URL to extract the values from + * @return an array containing the BPN number [0], purpose [1] and credential index [2] + */ + public String[] extractValuesFromURL(String url) { + // Define a regular expression pattern to match the desired parts + Pattern pattern = + Pattern.compile("/credentials/(B\\w+)/(.*?)/(\\d+)", Pattern.CASE_INSENSITIVE); + // Create a Matcher object and find the first match in the URL + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + String bpnlNumber = matcher.group(1); + String purpose = matcher.group(2); + String credentialIndex = matcher.group(3); + return new String[]{ bpnlNumber.toUpperCase(), purpose, credentialIndex }; + } else { + throw new IllegalArgumentException("No match found"); + } + } +} diff --git a/revocation-service/src/main/resources/application.yaml b/revocation-service/src/main/resources/application.yaml index 98f4296f..35065c0b 100644 --- a/revocation-service/src/main/resources/application.yaml +++ b/revocation-service/src/main/resources/application.yaml @@ -21,7 +21,7 @@ revocation: application: name: ${APPLICATION_NAME:Revocation} - port: ${APPLICATION_PORT:8080} + port: ${APPLICATION_PORT:8081} profile: ${APPLICATION_PROFILE:local} database: host: ${DATABASE_HOST:localhost} @@ -49,10 +49,10 @@ revocation: token-url: ${revocation.security.keycloak.auth-server-url}/realms/${revocation.security.keycloak.realm}/protocol/openid-connect/token refresh-token-url: ${revocation.security.keycloak.token-url} miw: - url: ${MIW_URL:https://b366-203-129-213-107.ngrok-free.app} - vcContexts: ${VC_SCHEMA_LINK:https://www.w3.org/2018/credentials/v1, https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json} + url: ${MIW_URL:https://a888-203-129-213-107.ngrok-free.app} + vcContexts: ${VC_SCHEMA_LINK:https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json} domain: - url: ${DOMAIN_URL:http://localhost:8080} + url: ${DOMAIN_URL:https://977d-203-129-213-107.ngrok-free.app} diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java index bbb67685..6dc27476 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java @@ -21,6 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.revocation; +import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusListCredentialSubject; import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListCredential; import org.eclipse.tractusx.managedidentitywallets.revocation.jpa.StatusListIndex; @@ -51,6 +52,8 @@ import java.util.Map; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; import static org.mockito.Mockito.when; @@ -186,4 +189,15 @@ public static BitSet decompressGzip(byte[] bytes) { public static String extractBpnFromDid(String did) { return did.substring(did.lastIndexOf(":") + 1).toUpperCase(); } + + @SneakyThrows + public static void main(String[] args) { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + // use explicit initialization as the platform default might fail + keyGen.init(128); + SecretKey secretKey = keyGen.generateKey(); + String s = Base64.getEncoder().encodeToString(secretKey.getEncoded()); + + System.out.println(s); + } } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java index 9e4e07c0..8beebbd0 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/ExceptionHandlingTest.java @@ -21,8 +21,8 @@ package org.eclipse.tractusx.managedidentitywallets.revocation.config; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.revocation.exception.CredentialAlreadyRevokedException; -import org.eclipse.tractusx.managedidentitywallets.revocation.exception.ForbiddenException; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java index 4d0740c7..ff11b541 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java @@ -22,13 +22,13 @@ package org.eclipse.tractusx.managedidentitywallets.revocation.controllers; import com.fasterxml.jackson.databind.ObjectMapper; -import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.RevocationPurpose; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; import org.eclipse.tractusx.managedidentitywallets.revocation.services.RevocationService; import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; -import org.eclipse.tractusx.managedidentitywallets.revocation.utils.StringPool; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -65,7 +65,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ExtendWith(MockitoExtension.class) -public class RevocationApiControllerTest { +class RevocationApiControllerTest { private static final String CALLER_BPN = UUID.randomUUID().toString(); @@ -87,16 +87,16 @@ public void setup() { } @Test - public void whenPostCreateStatusListVC_thenReturnStatus() throws Exception { + void whenPostCreateStatusListVC_thenReturnStatus() throws Exception { // Given - String validPurpose = PurposeType.REVOCATION.toString(); + String validPurpose = RevocationPurpose.REVOCATION.name(); StatusEntryDto statusEntryDto = new StatusEntryDto(validPurpose, DID); String validIndex = String.valueOf(BitSetManager.BITSET_SIZE / 2); // any valid index within range CredentialStatusDto credentialStatusDto = new CredentialStatusDto( "https://example.com/revocations/credentials/" + BPN + "/revocation/1#" + validIndex, - PurposeType.REVOCATION.toString(), + RevocationPurpose.REVOCATION.name(), validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] "https://example.com/revocations/credentials/" + BPN + "/revocation/1", "BitstringStatusListEntry"); @@ -132,7 +132,7 @@ private Principal mockPrincipal(String name) { } @Test - public void whenPostRevokeCredential_thenReturnOkStatus() throws Exception { + void whenPostRevokeCredential_thenReturnOkStatus() throws Exception { // Given String validIndex = String.valueOf(BitSetManager.BITSET_SIZE / 2); // any valid index within range @@ -160,9 +160,9 @@ public void whenPostRevokeCredential_thenReturnOkStatus() throws Exception { } @Test - public void whenGetCredential_thenReturnCredentials() throws Exception { + void whenGetCredential_thenReturnCredentials() throws Exception { // Given - String validPurpose = PurposeType.REVOCATION.toString(); + String validPurpose = RevocationPurpose.REVOCATION.name(); VerifiableCredential verifiableCredential = new VerifiableCredential( createVerifiableCredentialTestData()); // Populate with valid test data diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java index cdec1660..017d06b7 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/domain/BPNTest.java @@ -27,15 +27,14 @@ import static org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil.BPN; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -public class BPNTest { +class BPNTest { @Test @DisplayName("BPN Should not be valid") - public void invalidBPN() { + void invalidBPN() { String bpn = "thisnotbpn"; assertThrows( @@ -47,7 +46,7 @@ public void invalidBPN() { @Test @DisplayName("BPN Should be valid") - public void validBPN() { + void validBPN() { assertDoesNotThrow( () -> { new BPN(BPN); @@ -56,24 +55,24 @@ public void validBPN() { @Test @DisplayName("BPN Should return value") - public void bpnValue() { + void bpnValue() { BPN bpn = new BPN(BPN); assertEquals(BPN, bpn.value()); } @Test @DisplayName("BPN Should be equal") - public void bpnEqual() { + void bpnEqual() { BPN bpn1 = new BPN(BPN); BPN bpn2 = new BPN(BPN); - assertTrue(bpn1.equals(bpn2)); + assertEquals(bpn1, bpn2); } @Test @DisplayName("BPN Should not be equal") - public void bpnNotEqual() { + void bpnNotEqual() { BPN bpn1 = new BPN(BPN); BPN bpn2 = new BPN("BPNL000000000000"); - assertFalse(bpn1.equals(bpn2)); + assertNotEquals(bpn1, bpn2); } } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java index 5e95b108..ff11b2fc 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusEntryDtoTest.java @@ -23,7 +23,7 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; -import org.eclipse.tractusx.managedidentitywallets.revocation.constant.PurposeType; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.RevocationPurpose; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,7 +36,7 @@ class StatusEntryDtoTest { @Test void validStatusEntryDto_CreatesSuccessfully() { // Arrange - String validPurpose = PurposeType.REVOCATION.toString(); + String validPurpose = RevocationPurpose.REVOCATION.name(); // Act StatusEntryDto dto = new StatusEntryDto(validPurpose, "issuerId"); @@ -81,7 +81,7 @@ void anyParameterIsBlank_ThrowsValidationException() { validator .validate( new StatusEntryDto( - PurposeType.REVOCATION.toString(), "" // issuerId is blank + RevocationPurpose.REVOCATION.name(), "" // issuerId is blank )) .isEmpty()); } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java index d1fa870c..246c8473 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java @@ -28,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -public class StatusListCredentialSubjectTest { +class StatusListCredentialSubjectTest { @Test void builderCreatesObjectWithCorrectValues() { diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java index e163cce0..39c1a035 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/jpa/StatusListIndexTest.java @@ -42,7 +42,7 @@ @ExtendWith(SpringExtension.class) @AutoConfigureJson @DataJpaTest -public class StatusListIndexTest { +class StatusListIndexTest { @Autowired private TestEntityManager entityManager; @@ -56,7 +56,7 @@ public void setUp() { } @Test - public void whenFieldsAreValid_thenNoConstraintViolationsAndEntityPersists() { + void whenFieldsAreValid_thenNoConstraintViolationsAndEntityPersists() { String id = BPN + "-revocation#1"; String issuerBpnStatus = BPN + "-revocation"; @@ -79,7 +79,7 @@ public void whenFieldsAreValid_thenNoConstraintViolationsAndEntityPersists() { } @Test - public void whenIdIsBlank_thenConstraintViolationOccurs() { + void whenIdIsBlank_thenConstraintViolationOccurs() { StatusListIndex statusListIndex = StatusListIndex.builder().id(" ").currentIndex("123456").build(); Set> violations = validator.validate(statusListIndex); @@ -89,7 +89,7 @@ public void whenIdIsBlank_thenConstraintViolationOccurs() { } @Test - public void whenIssuerBpnStatusIsBlank_thenConstraintViolationOccurs() { + void whenIssuerBpnStatusIsBlank_thenConstraintViolationOccurs() { StatusListIndex statusListIndex = StatusListIndex.builder() .issuerBpnStatus(" ") @@ -103,7 +103,7 @@ public void whenIssuerBpnStatusIsBlank_thenConstraintViolationOccurs() { } @Test - public void whenCurrentIndexIsBlank_thenConstraintViolationOccurs() { + void whenCurrentIndexIsBlank_thenConstraintViolationOccurs() { StatusListIndex statusListIndex = StatusListIndex.builder().issuerBpnStatus(TestUtil.BPN).currentIndex(" ").build(); Set> violations = validator.validate(statusListIndex); @@ -113,7 +113,7 @@ public void whenCurrentIndexIsBlank_thenConstraintViolationOccurs() { } @Test - public void whenCurrentIndexIsNotNumeric_thenConstraintViolationOccurs() { + void whenCurrentIndexIsNotNumeric_thenConstraintViolationOccurs() { String id = BPN + "-revocation#1"; String issuerBpnStatus = BPN + "-revocation"; String wrongIndex = "indexABC"; @@ -131,7 +131,7 @@ public void whenCurrentIndexIsNotNumeric_thenConstraintViolationOccurs() { } @Test - public void whenSetInvalidCurrentIndex_thenIllegalArgumentExceptionIsThrown() { + void whenSetInvalidCurrentIndex_thenIllegalArgumentExceptionIsThrown() { // Constructing StatusListIndex using the builder pattern // with a valid issuerId and leaving currentIndex unset initially StatusListIndex statusListIndex = @@ -143,7 +143,7 @@ public void whenSetInvalidCurrentIndex_thenIllegalArgumentExceptionIsThrown() { } @Test - public void whenFieldsExceedSizeLimit_thenConstraintViolationOccurs() { + void whenFieldsExceedSizeLimit_thenConstraintViolationOccurs() { String longIssuerBpnStatus = BPN + "-revocation1"; String longCurrentIndex = "12345".repeat(4); // The repeat count adjusts on the max size of Index diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java deleted file mode 100644 index 159e1fd6..00000000 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/ValidateTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ******************************************************************************* - * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ****************************************************************************** - */ - -package org.eclipse.tractusx.managedidentitywallets.revocation.utils; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ValidateTest { - - @Test - void validateTest() { - Assertions.assertThrows( - RuntimeException.class, () -> Validate.isFalse(false).launch(new RuntimeException())); - Assertions.assertThrows( - RuntimeException.class, () -> Validate.isTrue(true).launch(new RuntimeException())); - Assertions.assertThrows( - RuntimeException.class, () -> Validate.isNull(null).launch(new RuntimeException())); - Assertions.assertThrows( - RuntimeException.class, () -> Validate.isNotNull("Test").launch(new RuntimeException())); - Assertions.assertThrows( - RuntimeException.class, - () -> Validate.value("").isNotEmpty().launch(new RuntimeException())); - Assertions.assertDoesNotThrow(() -> Validate.isFalse(true).launch(new RuntimeException())); - Assertions.assertDoesNotThrow(() -> Validate.isTrue(false).launch(new RuntimeException())); - Assertions.assertDoesNotThrow(() -> Validate.isNull("").launch(new RuntimeException())); - Assertions.assertDoesNotThrow(() -> Validate.isNotNull(null).launch(new RuntimeException())); - Assertions.assertDoesNotThrow( - () -> Validate.value("Test").isNotEmpty().launch(new RuntimeException())); - } -} diff --git a/settings.gradle b/settings.gradle index f93bc271..b50d694b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,4 @@ rootProject.name = 'managedidentitywallets' // include '' include 'miw' include 'revocation-service' +include 'wallet-commons' diff --git a/wallet-commons/build.gradle b/wallet-commons/build.gradle new file mode 100644 index 00000000..45119542 --- /dev/null +++ b/wallet-commons/build.gradle @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +plugins { + id 'java-library' + id 'jacoco' + id 'maven-publish' +} + +dependencies { + + testImplementation "org.testcontainers:junit-jupiter" + testImplementation 'org.junit.jupiter:junit-jupiter-api' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + mavenBom("org.testcontainers:testcontainers-bom:${testContainerVersion}") + } +} + + +jar { + enabled = true + archiveClassifier = '' +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/ApplicationRole.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java similarity index 92% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/ApplicationRole.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java index a534ad42..581d285a 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/ApplicationRole.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -19,7 +19,7 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.constant; +package org.eclipse.tractusx.managedidentitywallets.commons.constant; public class ApplicationRole { diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java similarity index 76% rename from revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java index 11722b79..ea5503f9 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/constant/ApplicationRole.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java @@ -19,15 +19,18 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.revocation.constant; +package org.eclipse.tractusx.managedidentitywallets.commons.constant; -public class ApplicationRole { +import lombok.Getter; - public static final String ROLE_MANAGE_APP = "manage_app"; +@Getter +public enum CredentialStatus { - public static final String ROLE_UPDATE_WALLET = "update_wallet"; + ACTIVE("active"), REVOKED("revoked"); - private ApplicationRole() { - // static + private final String name; + + CredentialStatus(String name) { + this.name = name; } } diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java new file mode 100644 index 00000000..608ec2f5 --- /dev/null +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java @@ -0,0 +1,32 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.commons.constant; + +public enum RevocationPurpose { + REVOCATION("revocation"), SUSPENSION("suspension"); + + private final String name; + + RevocationPurpose(String name) { + this.name = name; + } +} diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java similarity index 93% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java index f63ae081..d6695fdd 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/StringPool.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java @@ -19,13 +19,14 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.constant; +package org.eclipse.tractusx.managedidentitywallets.commons.constant; -/** - * The type Application constant. - */ +import lombok.experimental.UtilityClass; + +@UtilityClass public class StringPool { + public static final String CREDENTIAL_ID = "credentialId"; public static final String VERIFIABLE_CREDENTIALS = "verifiableCredentials"; @@ -37,10 +38,6 @@ public class StringPool { public static final String VALIDATE_JWT_EXPIRY_DATE = "validateJWTExpiryDate"; public static final String DID_DOCUMENT = "didDocument"; - private StringPool() { - throw new IllegalStateException("Constant class"); - } - public static final String ISSUER_DID = "issuerDid"; public static final String HOLDER_DID = "holderDid"; public static final String HOLDER_IDENTIFIER = "holderIdentifier"; @@ -102,4 +99,9 @@ private StringPool() { public static final String CREDENTIAL_SERVICE = "CredentialService"; public static final String HTTPS_SCHEME = "https://"; public static final String BPN_NOT_FOUND = "BPN not found"; + + public static final String REVOCABLE = "revocable"; + + public static final String CREDENTIAL_STATUS = "credentialStatus"; + } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/SupportedAlgorithms.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java similarity index 91% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/SupportedAlgorithms.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java index 227fa134..7787937b 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/SupportedAlgorithms.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java @@ -19,7 +19,7 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.constant; +package org.eclipse.tractusx.managedidentitywallets.commons.constant; public enum SupportedAlgorithms { @@ -28,7 +28,7 @@ public enum SupportedAlgorithms { private String value; - SupportedAlgorithms(String value){ + SupportedAlgorithms(String value) { this.value = value; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/TokenValidationErrors.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/TokenValidationErrors.java similarity index 94% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/TokenValidationErrors.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/TokenValidationErrors.java index e90b784c..571a3cd2 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/constant/TokenValidationErrors.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/TokenValidationErrors.java @@ -19,7 +19,7 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.constant; +package org.eclipse.tractusx.managedidentitywallets.commons.constant; public enum TokenValidationErrors { diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/BadDataException.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataException.java similarity index 92% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/BadDataException.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataException.java index 79cd5791..050923ee 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/exception/BadDataException.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataException.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -19,7 +19,7 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.exception; +package org.eclipse.tractusx.managedidentitywallets.commons.exception; /** * The type Bad data exception. diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenException.java similarity index 93% rename from revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenException.java index a5420c34..81840357 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/exception/ForbiddenException.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenException.java @@ -19,8 +19,11 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.revocation.exception; +package org.eclipse.tractusx.managedidentitywallets.commons.exception; +/** + * The type Forbidden exception. + */ public class ForbiddenException extends RuntimeException { /** diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/utils/Validate.java similarity index 98% rename from revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/utils/Validate.java index 724153dc..e32fe86c 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/Validate.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/utils/Validate.java @@ -19,7 +19,7 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.revocation.utils; +package org.eclipse.tractusx.managedidentitywallets.commons.utils; import java.util.Objects; @@ -102,6 +102,7 @@ public static Validate isNotNull(T value) { return new Validate<>(value).isNotNull(); } + /** * Is not empty validate. * diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/ValidateTest.java b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java similarity index 91% rename from miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/ValidateTest.java rename to wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java index 9ba28ec8..4a65cb34 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/ValidateTest.java +++ b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java @@ -1,6 +1,6 @@ /* * ******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -19,8 +19,9 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.utils; +package org.eclipse.tractusx.managedidentitywallets.commons; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; From b1c54176cfba899fbcfed32dc1d028cc028e0a68 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Fri, 14 Jun 2024 19:08:12 +0530 Subject: [PATCH 20/60] fix: sonar issues --- .../service/PresentationService.java | 7 +------ .../managedidentitywallets/service/WalletService.java | 8 ++------ .../signing/LocalSigningServiceImpl.java | 4 ++-- .../managedidentitywallets/utils/AuthenticationUtils.java | 7 ++++++- .../revocation/controllers/RevocationApiController.java | 2 +- .../validation/VerifiableCredentialValidator.java | 8 +------- .../security/CustomAuthenticationConverterTest.java | 6 +++--- .../revocation/utils/BitSetManagerTest.java | 4 ++-- .../commons/constant/StringPool.java | 3 --- .../commons/constant/SupportedAlgorithms.java | 2 +- 10 files changed, 19 insertions(+), 32 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java index 57e7d41f..e5c1b169 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java @@ -274,12 +274,7 @@ private boolean validateCredential(VerifiableCredential credential) { final DidResolver resolver = didDocumentResolverService.getCompositeDidResolver(); final LinkedDataProofValidation linkedDataProofValidation = LinkedDataProofValidation.newInstance(resolver); final boolean isValid = linkedDataProofValidation.verify(credential); - - if (isValid) { - log.debug("Credential validation result: (valid: {}, credential-id: {})", isValid, credential.getId()); - } else { - log.info("Credential validation result: (valid: {}, credential-id: {})", isValid, credential.getId()); - } + log.info("Credential validation result: (valid: {}, credential-id: {})", isValid, credential.getId()); return isValid; } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java index e3a835b3..76237e05 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java @@ -363,11 +363,7 @@ private void validateCreateWallet(CreateWalletRequest request, String callerBpn) } - @RequiredArgsConstructor - private class WalletKeyInfo { - private final String keyId; - private final KeyPair keyPair; - private final SupportedAlgorithms algorithm; - private final VerificationMethod verificationMethod; + private record WalletKeyInfo(String keyId, KeyPair keyPair, SupportedAlgorithms algorithm, + VerificationMethod verificationMethod) { } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java index d3f6ae05..5317ec82 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/signing/LocalSigningServiceImpl.java @@ -278,7 +278,7 @@ private SignedJWT generateJwtPresentation(PresentationCreationConfig config, byt private VerifiablePresentation generateJsonLdPresentation(PresentationCreationConfig config, byte[] privateKeyBytes) throws UnsupportedSignatureTypeException, InvalidPrivateKeyFormatException, SignatureGenerateFailedException, TransformJsonLdException { VerifiablePresentationBuilder verifiablePresentationBuilder = - new VerifiablePresentationBuilder().id(URI.create(config.getVpIssuerDid() + "#" + UUID.randomUUID().toString())) + new VerifiablePresentationBuilder().id(URI.create(config.getVpIssuerDid() + "#" + UUID.randomUUID())) .type(List.of(VerifiablePresentationType.VERIFIABLE_PRESENTATION)) .verifiableCredentials(config.getVerifiableCredentials()); @@ -339,7 +339,7 @@ private SignedJWT buildED25519(PresentationCreationConfig config, byte[] private SerializedJwtPresentationFactory presentationFactory = new SerializedJwtPresentationFactoryImpl( new SignedJwtFactory(new OctetKeyPairFactory()), new JsonLdSerializerImpl(), config.getVpIssuerDid()); - X25519PrivateKey privateKey = null; + X25519PrivateKey privateKey; try { privateKey = new X25519PrivateKey(privateKeyBytes); } catch (InvalidPrivateKeyFormatException e) { diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java index 6f2a31ce..6a11009a 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/AuthenticationUtils.java @@ -157,10 +157,15 @@ private static String getJwtToken(String username) { .clientId(StringPool.CLIENT_ID) .clientSecret(StringPool.CLIENT_SECRET) .username(username) - .password(StringPool.USER_PASSWORD) + .password(getUserPassword()) .build(); String access_token = keycloakAdminClient.tokenManager().getAccessToken().getToken(); return StringPool.BEARER_SPACE + access_token; } + + @NotNull + private static String getUserPassword() { + return "s3cr3t"; + } } diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java index 580ff2af..8c1ed619 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiController.java @@ -131,7 +131,7 @@ public ResponseEntity> verifyRevocation( /** * The function `getCredentialsByIssuerId` retrieves a list of credentials by their issuer ID. * - * @param issuerId The `issuerId` parameter is a string that represents the identifier of the + * @param issuerBPN The `issuerBPN` parameter is a string that represents the BPn of the * issuer. * @return The method is returning a ResponseEntity object that wraps a VerifiableCredential * object. diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java index 66372d57..00947e53 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java @@ -48,15 +48,9 @@ public boolean isValid(VerifiableCredential credential, ConstraintValidatorConte // Assuming 'id' within 'credentialSubject' is the field to be validated as a URI @NonNull List credentialSubject = credential.getCredentialSubject(); - if (credentialSubject != null) { - if (!validateCredentialSubject(credentialSubject, context)) { - return false; - } - } + return validateCredentialSubject(credentialSubject, context); // Additional validation checks can be added here, e.g., checking the proof object - - return true; } private boolean validateCredentialSubject( diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java index 29de8663..a918b0fe 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java @@ -36,13 +36,13 @@ class CustomAuthenticationConverterTest { - private static String VALID = + private static final String VALID = "eyJhbGciOiJSUzI1NiIsImFscGhhIjoiZzB1ZjNycjlycnN2cHlhcTVuamg4In0.eyJpc3MiOiJEaW5vQ2hpZXNhLmdpdGh1Yi5pbyIsInN1YiI6Im1heGluZSIsImF1ZCI6ImlkcmlzIiwiaWF0IjoxNzAyNjUwMTc2LCJleHAiOjE3MDI2NTA3NzYsInJlc291cmNlX2FjY2VzcyI6eyJyZXNvdXJjZUlkIjp7InJvbGVzIjpbImRlaV9tdWRhIl19fX0.wTv9GBX3AuRO8UIsAuu2YJU77ai-wchDyxRn-_yX9PeHt23vCmp_JAbkkdMdyLAWWOKncjgNeG-4lB9RCBsjmbdb1imujUrAocp3VZQqNg6OVaNV58kdsIpNNF9S8XlFI4hr1BANrw2rWJDkTRu1id-Fu-BVE1BF7ySCKHS_NaY3e7yXQM-jtU63z5FBpPvfMF-La3blPle93rgut7V3LlG-tNOp93TgFzGrQQXuJUsew34T0u4OlQa3TjQuMdZMTy0SVSLSpIzAqDsAkHv34W6SdY1p6FVQ14TfawRLkrI2QY-YM_dCFAEE7KqqnUrVVyw6XG1ydeFDuX8SJuQX7g"; - private static String MISSING_RESOURCE_ID = + private static final String MISSING_RESOURCE_ID = "{\n" + " \"resource_access\": {\n" + " }\n" + "}"; - private static String MISSING_ROLES = + private static final String MISSING_ROLES = "{\n" + " \"resource_access\": {\n" + " \"resourceId\": {\n" diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java index 6e41da99..67f9c813 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/utils/BitSetManagerTest.java @@ -24,9 +24,9 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.util.Arrays; import java.util.BitSet; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -104,6 +104,6 @@ void encodeToStringAndDecodeFromString_AreReversible() { byte[] decodedData = BitSetManager.decodeFromString(encoded); assertNotNull(decodedData); - assertTrue(Arrays.equals(originalData, decodedData)); + assertArrayEquals(originalData, decodedData); } } diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java index d6695fdd..5ee3fd5e 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java @@ -62,9 +62,6 @@ public class StringPool { public static final String CLIENT_SECRET = "miw_private_client_secret"; public static final String REALM = "miw_test"; - - public static final String USER_PASSWORD = "s3cr3t"; - public static final String VALID_USER_NAME = "valid_user"; public static final String INVALID_USER_NAME = "invalid_user"; diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java index 7787937b..b9750cfe 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java @@ -26,7 +26,7 @@ public enum SupportedAlgorithms { ED25519("ED25519"), ES256K("ES256K"); - private String value; + private final String value; SupportedAlgorithms(String value) { this.value = value; From 02ccd3112bbed9ff14f8f487a5df40eaced79eba Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Fri, 14 Jun 2024 19:54:06 +0530 Subject: [PATCH 21/60] fix: test cases due to revocation client --- gradle.properties | 1 + miw/build.gradle | 2 +- .../utils/TestUtils.java | 142 ++++++++++++++++++ .../vc/HoldersCredentialTest.java | 22 ++- .../vc/IssuersCredentialTest.java | 24 +++ .../vp/PresentationServiceTest.java | 24 +++ .../vp/PresentationTest.java | 22 +++ 7 files changed, 235 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 6481bbad..9b250fba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,4 @@ ssiLibVersion=0.0.19 wiremockVersion=3.4.2 commonsDaoVersion=0.0.5 appGroup=org.eclipse.tractusx.managedidentitywallets +mockInBeanVersion=boot2-v1.5.2 diff --git a/miw/build.gradle b/miw/build.gradle index 590d9f2f..9b70d146 100644 --- a/miw/build.gradle +++ b/miw/build.gradle @@ -70,7 +70,7 @@ dependencies { testImplementation group: 'org.mockito', name: 'mockito-inline', version: '5.2.0' testImplementation group: 'org.json', name: 'json', version: '20230227' testImplementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: '1.4' - + testImplementation "com.teketik:mock-in-bean:${mockInBeanVersion}" } dependencyManagement { diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 5e9a7e34..5f1d85b7 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -33,6 +33,8 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import lombok.SneakyThrows; +import org.apache.commons.lang3.RandomUtils; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; @@ -41,10 +43,13 @@ import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; +import org.eclipse.tractusx.managedidentitywallets.dto.StatusListRequest; +import org.eclipse.tractusx.managedidentitywallets.revocation.RevocationClient; import org.eclipse.tractusx.ssi.lib.model.did.DidDocument; import org.eclipse.tractusx.ssi.lib.model.verifiable.Verifiable; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialBuilder; +import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialStatusList2021Entry; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialType; import org.jetbrains.annotations.NotNull; @@ -52,6 +57,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -59,14 +65,19 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URI; import java.time.Instant; import java.util.ArrayList; +import java.util.Base64; +import java.util.BitSet; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.zip.GZIPOutputStream; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; @@ -265,4 +276,135 @@ public static Map getCredentialAsMap(String holderBpn, String ho return objectMapper.readValue(credentialWithoutProof.toJson(), Map.class); } + + + public static VerifiableCredentialStatusList2021Entry getStatusListEntry(int index) { + return new VerifiableCredentialStatusList2021Entry(Map.of( + "id", "https://example.com/credentials/bpn123456789000/revocation/3#" + index, + "type", "BitstringStatusListEntry", + "statusPurpose", "revocation", + "statusListIndex", String.valueOf(index), + "statusListCredential", "https://example.com/credentials/bpn123456789000/revocation/3" + )); + } + + public static VerifiableCredentialStatusList2021Entry getStatusListEntry() { + int index = RandomUtils.nextInt(1, 100); + return new VerifiableCredentialStatusList2021Entry(Map.of( + "id", "https://example.com/credentials/bpn123456789000/revocation/3#" + index, + "type", "BitstringStatusListEntry", + "statusPurpose", "revocation", + "statusListIndex", String.valueOf(index), + "statusListCredential", "https://example.com/credentials/bpn123456789000/revocation/3" + )); + } + + public static void mockGetStatusListEntry(RevocationClient revocationClient, int statusIndex) { + //mock revocation service + Mockito.when(revocationClient.getStatusListEntry(Mockito.any(StatusListRequest.class), Mockito.any(String.class))).thenReturn(TestUtils.getStatusListEntry(statusIndex)); + } + + public static void mockGetStatusListEntry(RevocationClient revocationClient) { + //mock revocation service + Mockito.when(revocationClient.getStatusListEntry(Mockito.any(StatusListRequest.class), Mockito.any(String.class))).thenReturn(TestUtils.getStatusListEntry()); + } + + + public static void mockRevocationVerification(RevocationClient revocationClient, CredentialStatus credentialStatus) { + Mockito.when(revocationClient.verifyCredentialStatus(Mockito.any(), Mockito.anyString())).thenReturn(Map.of("status", credentialStatus.getName().toLowerCase())); + } + + @SneakyThrows + public static void mockGetStatusListVC(RevocationClient revocationClient, ObjectMapper objectMapper, String encodedList) { + String vcString = """ + { + "type": [ + "VerifiableCredential" + ], + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "http://localhost:8085/api/v1/revocations/credentials/did:web:BPNL01-revocation", + "issuer": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "issuanceDate": "2023-11-30T11:29:17Z", + "issued": "2023-11-30T11:29:17Z", + "validFrom": "2023-11-30T11:29:17Z", + "proof": { + "type": "JsonWebSignature2020", + "created": "2023-11-30T11:29:17Z", + "verificationMethod": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce#z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFZERTQSJ9..Iv6H_e4kfLj9dr0COsB2D_ZPpkMoFj3BVXW2iyKFC3q5QtvPWraWfzEDJ5fxtfd5bARJQIP6YhaXdfSRgJpACQ" + }, + "credentialSubject": { + "id": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "type": "BitstringStatusList", + "statusPurpose": "revocation", + "encodedList": "##encodedList" + } + } + """; + vcString = vcString.replace("##encodedList", encodedList); + + VerifiableCredential verifiableCredential = new VerifiableCredential(objectMapper.readValue(vcString, Map.class)); + Mockito.when(revocationClient.getStatusListCredential(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class))).thenReturn(verifiableCredential); + } + + @SneakyThrows + public static void mockGetStatusListVC(RevocationClient revocationClient, ObjectMapper objectMapper) { + String vcString = """ + { + "type": [ + "VerifiableCredential", + "StatusList2021Credential" + ], + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "http://localhost:8085/api/v1/revocations/credentials/did:web:BPNL01-revocation", + "issuer": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "issuanceDate": "2023-11-30T11:29:17Z", + "issued": "2023-11-30T11:29:17Z", + "validFrom": "2023-11-30T11:29:17Z", + "proof": { + "type": "JsonWebSignature2020", + "creator": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "created": "2023-11-30T11:29:17Z", + "verificationMethod": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce#z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFZERTQSJ9..Iv6H_e4kfLj9dr0COsB2D_ZPpkMoFj3BVXW2iyKFC3q5QtvPWraWfzEDJ5fxtfd5bARJQIP6YhaXdfSRgJpACQ" + }, + "credentialSubject": { + "id": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", + "type": "StatusList2021Credential", + "statusPurpose": "revocation", + "encodedList": "H4sIAAAAAAAA/+3BMQEAAAjAoEqzfzk/SwjUmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDXFiqoX4AAAAIA" + } + } + """; + + VerifiableCredential verifiableCredential = new VerifiableCredential(objectMapper.readValue(vcString, Map.class)); + Mockito.when(revocationClient.getStatusListCredential(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class), Mockito.any(String.class))).thenReturn(verifiableCredential); + } + + public static String createEncodedList() throws IOException { + BitSet bitSet = new BitSet(16 * 1024 * 8); + + byte[] bitstringBytes = bitSet.toByteArray(); + // Perform GZIP compression + ByteArrayOutputStream gzipOutput = new ByteArrayOutputStream(); + try (GZIPOutputStream gzipStream = new GZIPOutputStream(gzipOutput)) { + gzipStream.write(bitstringBytes); + } + + + // Base64 encode the compressed byte array + byte[] compressedBytes = gzipOutput.toByteArray(); + String encodedList = Base64.getEncoder().encodeToString(compressedBytes); + + + return encodedList; + } } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java index fa74e9c0..624eb5d6 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java @@ -23,8 +23,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.teketik.test.mockinbean.MockInBean; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; @@ -36,6 +38,8 @@ import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; +import org.eclipse.tractusx.managedidentitywallets.revocation.RevocationClient; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; @@ -54,7 +58,9 @@ import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofValidation; import org.eclipse.tractusx.ssi.lib.serialization.SerializeUtil; import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -95,9 +101,24 @@ class HoldersCredentialTest { @Autowired private TestRestTemplate restTemplate; + @MockInBean(RevocationService.class) + private RevocationClient revocationClient; + @Autowired private IssuersCredentialController credentialController; + @SneakyThrows + @BeforeEach + void beforeEach() { + TestUtils.mockGetStatusListEntry(revocationClient); + TestUtils.mockGetStatusListVC(revocationClient, objectMapper, TestUtils.createEncodedList()); + TestUtils.mockRevocationVerification(revocationClient, CredentialStatus.ACTIVE); + } + + @AfterEach + void afterEach() { + Mockito.reset(revocationClient); + } @Test void issueCredentialTestWithInvalidBPNAccess403() throws JsonProcessingException { @@ -122,7 +143,6 @@ void issueCredentialTest200() throws JsonProcessingException { ResponseEntity response = issueVC(bpn, did, type, headers); - Assertions.assertEquals(HttpStatus.CREATED.value(), response.getStatusCode().value()); VerifiableCredential verifiableCredential = new VerifiableCredential(new ObjectMapper().readValue(response.getBody(), Map.class)); Assertions.assertNotNull(verifiableCredential.getProof()); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java index 712dcf5c..92fc8b40 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java @@ -24,7 +24,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.teketik.test.mockinbean.MockInBean; +import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; @@ -37,15 +40,20 @@ import org.eclipse.tractusx.managedidentitywallets.dao.repository.IssuersCredentialRepository; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; +import org.eclipse.tractusx.managedidentitywallets.revocation.RevocationClient; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; import org.eclipse.tractusx.ssi.lib.did.web.DidWebFactory; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.serialization.SerializeUtil; import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -82,6 +90,22 @@ class IssuersCredentialTest { private IssuersCredentialRepository issuersCredentialRepository; + @MockInBean(RevocationService.class) + private RevocationClient revocationClient; + + @SneakyThrows + @BeforeEach + void beforeEach() { + TestUtils.mockGetStatusListEntry(revocationClient); + TestUtils.mockGetStatusListVC(revocationClient, objectMapper, TestUtils.createEncodedList()); + TestUtils.mockRevocationVerification(revocationClient, CredentialStatus.ACTIVE); + } + + @AfterEach + void afterEach() { + Mockito.reset(revocationClient); + } + @Test void getCredentials200() throws com.fasterxml.jackson.core.JsonProcessingException, JSONException { String baseBPN = miwSettings.authorityWalletBpn(); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java index df45f40a..3e92c03d 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationServiceTest.java @@ -26,8 +26,10 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; +import com.teketik.test.mockinbean.MockInBean; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; @@ -38,8 +40,10 @@ import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; import org.eclipse.tractusx.managedidentitywallets.exception.MissingVcTypesException; import org.eclipse.tractusx.managedidentitywallets.exception.PermissionViolationException; +import org.eclipse.tractusx.managedidentitywallets.revocation.RevocationClient; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; import org.eclipse.tractusx.managedidentitywallets.service.PresentationService; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; import org.eclipse.tractusx.managedidentitywallets.utils.TestConstants; import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; @@ -47,8 +51,11 @@ import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -98,6 +105,23 @@ class PresentationServiceTest { @Autowired private WalletRepository walletRepository; + @MockInBean(RevocationService.class) + private RevocationClient revocationClient; + + @SneakyThrows + @BeforeEach + void beforeEach() { + TestUtils.mockGetStatusListEntry(revocationClient); + TestUtils.mockGetStatusListVC(revocationClient, objectMapper, TestUtils.createEncodedList()); + TestUtils.mockRevocationVerification(revocationClient, CredentialStatus.ACTIVE); + } + + @AfterEach + void afterEach() { + Mockito.reset(revocationClient); + } + + @SneakyThrows @Test void createPresentation200ResponseAsJWT() { diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java index e0b8e2f2..81fe85a5 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vp/PresentationTest.java @@ -25,8 +25,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; +import com.teketik.test.mockinbean.MockInBean; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; @@ -36,7 +38,9 @@ import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.HoldersCredentialRepository; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; +import org.eclipse.tractusx.managedidentitywallets.revocation.RevocationClient; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; +import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; import org.eclipse.tractusx.managedidentitywallets.utils.AuthenticationUtils; import org.eclipse.tractusx.managedidentitywallets.utils.TestUtils; import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; @@ -48,7 +52,9 @@ import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialType; import org.jetbrains.annotations.NotNull; import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedConstruction; import org.mockito.Mockito; @@ -101,6 +107,22 @@ class PresentationTest { @Autowired private WalletRepository walletRepository; + @MockInBean(RevocationService.class) + private RevocationClient revocationClient; + + @SneakyThrows + @BeforeEach + void beforeEach() { + TestUtils.mockGetStatusListEntry(revocationClient); + TestUtils.mockGetStatusListVC(revocationClient, objectMapper, TestUtils.createEncodedList()); + TestUtils.mockRevocationVerification(revocationClient, CredentialStatus.ACTIVE); + } + + @AfterEach + void afterEach() { + Mockito.reset(revocationClient); + } + @Test void validateVPAssJsonLd400() throws JsonProcessingException, JSONException { From e91b6a0330b65d09bc24b6c38086d17ee16a761c Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 14:00:51 +0530 Subject: [PATCH 22/60] fix: failing test cases --- .../config/RevocationSettings.java | 2 +- .../service/IssuersCredentialService.java | 12 ++++++++++++ miw/src/main/resources/application.yaml | 1 + .../service/IssuersCredentialServiceTest.java | 2 +- .../managedidentitywallets/utils/TestUtils.java | 10 +++++----- .../vc/HoldersCredentialTest.java | 6 +++++- .../vc/IssuersCredentialTest.java | 6 +++++- 7 files changed, 30 insertions(+), 9 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java index c36bb2ec..43b2a3a1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java @@ -26,5 +26,5 @@ import java.net.URI; @ConfigurationProperties(prefix = "miw.revocation") -public record RevocationSettings(URI url) { +public record RevocationSettings(URI url, URI bitStringStatusListContext) { } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java index dba5f6a9..6f1592dc 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java @@ -42,6 +42,7 @@ import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.IssuersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; @@ -77,6 +78,7 @@ import org.springframework.util.StringUtils; import java.io.IOException; +import java.net.URI; import java.net.http.HttpClient; import java.text.ParseException; import java.util.ArrayList; @@ -115,6 +117,8 @@ public class IssuersCredentialService extends BaseService getRepository() { @@ -239,6 +243,14 @@ public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map< //get credential status in case of revocation VerifiableCredentialStatusList2021Entry statusListEntry = revocationService.getStatusListEntry(issuerWallet.getBpn(), token); builder.verifiableCredentialStatus(statusListEntry); + + //add revocation context if missing + List uris = miwSettings.vcContexts(); + if (!uris.contains(revocationSettings.bitStringStatusListContext())) { + uris.add(revocationSettings.bitStringStatusListContext()); + builder.contexts(uris); + } + } CredentialCreationConfig holdersCredentialCreationConfig = builder.build(); diff --git a/miw/src/main/resources/application.yaml b/miw/src/main/resources/application.yaml index d705efe1..c40961bb 100644 --- a/miw/src/main/resources/application.yaml +++ b/miw/src/main/resources/application.yaml @@ -110,6 +110,7 @@ miw: refresh-token-url: ${miw.security.token-url} revocation: url: ${REVOCATION_SERVICE_URL:http://localhost:8081} + bitStringStatusListContext: ${BITSTRING_STATUS_LIST_CONTEXT_URL:https://w3c.github.io/vc-bitstring-status-list/contexts/v1.jsonld} sts: diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java index 4c52d70c..4127dc59 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java @@ -153,7 +153,7 @@ public static void beforeAll() throws SQLException { issuersCredentialRepository, miwSettings, new SpecificationUtil(), - holdersCredentialRepository, commonService, objectMapper, revocationService); + holdersCredentialRepository, commonService, objectMapper, revocationService, revocationSettings); } @BeforeEach diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 5f1d85b7..3eb7ed69 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -38,6 +38,7 @@ import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.WalletRepository; @@ -129,14 +130,14 @@ public static Wallet createWallet(String bpn, String did, WalletRepository walle return walletRepository.save(wallet); } - public static void checkVC(VerifiableCredential verifiableCredential, MIWSettings miwSettings) { - //text context URL - Assertions.assertEquals(verifiableCredential.getContext().size(), miwSettings.vcContexts().size() + 1); - + public static void checkVC(VerifiableCredential verifiableCredential, MIWSettings miwSettings, RevocationSettings revocationSettings) { for (URI link : miwSettings.vcContexts()) { Assertions.assertTrue(verifiableCredential.getContext().contains(link)); } + if (verifiableCredential.getVerifiableCredentialStatus() != null) { + Assertions.assertTrue(verifiableCredential.getContext().contains(revocationSettings.bitStringStatusListContext())); + } //check expiry date Assertions.assertEquals(0, verifiableCredential.getExpirationDate().compareTo(miwSettings.vcExpiryDate().toInstant())); } @@ -261,7 +262,6 @@ public static Map getCredentialAsMap(String holderBpn, String ho //VC Subject VerifiableCredentialSubject verifiableCredentialSubject = new VerifiableCredentialSubject(subjectData); - //Using Builder VerifiableCredential credentialWithoutProof = verifiableCredentialBuilder diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java index 624eb5d6..16efe6b5 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java @@ -29,6 +29,7 @@ import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.controller.IssuersCredentialController; @@ -101,6 +102,9 @@ class HoldersCredentialTest { @Autowired private TestRestTemplate restTemplate; + @Autowired + private RevocationSettings revocationSettings; + @MockInBean(RevocationService.class) private RevocationClient revocationClient; @@ -149,7 +153,7 @@ void issueCredentialTest200() throws JsonProcessingException { List credentials = holdersCredentialRepository.getByHolderDidAndType(did, type); Assertions.assertFalse(credentials.isEmpty()); - TestUtils.checkVC(credentials.get(0).getData(), miwSettings); + TestUtils.checkVC(credentials.get(0).getData(), miwSettings, revocationSettings); Assertions.assertTrue(credentials.get(0).isSelfIssued()); Assertions.assertFalse(credentials.get(0).isStored()); } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java index 92fc8b40..52f73c4d 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java @@ -31,6 +31,7 @@ import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; @@ -89,6 +90,9 @@ class IssuersCredentialTest { @Autowired private IssuersCredentialRepository issuersCredentialRepository; + @Autowired + private RevocationSettings revocationSettings; + @MockInBean(RevocationService.class) private RevocationClient revocationClient; @@ -259,7 +263,7 @@ void issueCredentials200() throws com.fasterxml.jackson.core.JsonProcessingExcep List credentials = holdersCredentialRepository.getByHolderDidAndType(did, type); Assertions.assertFalse(credentials.isEmpty()); - TestUtils.checkVC(credentials.get(0).getData(), miwSettings); + TestUtils.checkVC(credentials.get(0).getData(), miwSettings, revocationSettings); Assertions.assertFalse(credentials.get(0).isStored()); //stored must be false Assertions.assertFalse(credentials.get(0).isSelfIssued()); //stored must be false From e739cdc6be1cc8c0c9d3cf2dde8b1b2ef0055b22 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 15:48:25 +0530 Subject: [PATCH 23/60] fix: more test added --- .../services/RevocationService.java | 14 ++- .../services/RevocationServiceTest.java | 96 +++++++++++++++++++ .../services/StatusVerificationTest.java | 33 +++++++ .../commons/constant/StringPool.java | 2 + 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java index f0ebd35c..ab273c17 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java @@ -25,6 +25,8 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.eclipse.tractusx.managedidentitywallets.revocation.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; @@ -89,10 +91,19 @@ public class RevocationService { private final MIWSettings miwSettings; + /** + * Verifies the status of a credential based on the provided CredentialStatusDto object. + * + * @param statusDto The CredentialStatusDto object containing the necessary information for status verification. + * @return A Map object with the key "status" and the value "revoked" or "active" indicating the status of the credential. + * @throws BadDataException If the status list VC is not found for the issuer. + */ @Transactional public Map verifyStatus(CredentialStatusDto statusDto) { + validateCredentialStatus(statusDto); + String url = statusDto.statusListCredential(); String[] values = CommonUtils.extractValuesFromURL(url); @@ -106,13 +117,12 @@ public Map verifyStatus(CredentialStatusDto statusDto) { //validate status list VC validateStatusListVC(statusListCredential); - String encodedList = statusListCredential.getCredentialSubject().get(0).get(ENCODED_LIST).toString(); BitSet bitSet = BitSetManager.decompress(BitSetManager.decodeFromString(encodedList)); int index = Integer.parseInt(statusDto.statusListIndex()); boolean status = bitSet.get(index); - return Map.of("status", status ? "revoked" : "active"); + return Map.of(StringPool.STATUS, status ? CredentialStatus.REVOKED.getName() : CredentialStatus.ACTIVE.getName()); } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java index 3d98c148..728ec0d2 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java @@ -22,6 +22,9 @@ package org.eclipse.tractusx.managedidentitywallets.revocation.services; import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.SneakyThrows; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.revocation.TestUtil; import org.eclipse.tractusx.managedidentitywallets.revocation.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; @@ -33,8 +36,11 @@ import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListCredentialRepository; import org.eclipse.tractusx.managedidentitywallets.revocation.repository.StatusListIndexRepository; import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; +import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; +import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofValidation; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -49,6 +55,7 @@ import java.util.Base64; import java.util.BitSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -107,6 +114,95 @@ public void beforeEach() { Mockito.reset(statusListCredentialRepository, statusListIndexRepository, httpClientService); } + + @Nested + class VerifyStatusTest { + @SneakyThrows + @Test + void shouldVerifyStatusActive() { + final var issuer = DID; + var encodedList = mockEmptyEncodedList(); + var credentialBuilder = mockStatusListVC(issuer, "1", encodedList); + var statusListCredential = mockStatusListCredential(issuer, credentialBuilder); + // 1. create status list with the credential + var statusListIndex = mockStatusListIndex(issuer, statusListCredential, "0"); + when(statusListIndex.getStatusListCredential()).thenReturn(statusListCredential); + when(statusListCredentialRepository.findById(any(String.class))) + .thenReturn(Optional.of(statusListCredential)); + CredentialStatusDto credentialStatusDto = Mockito.mock(CredentialStatusDto.class); + when(credentialStatusDto.id()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1#0"); + when(credentialStatusDto.statusPurpose()).thenReturn("revocation"); + when(credentialStatusDto.statusListIndex()).thenReturn("0"); + when(credentialStatusDto.statusListCredential()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1"); + when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + + + try (MockedStatic utils = Mockito.mockStatic(LinkedDataProofValidation.class)) { + LinkedDataProofValidation mock = Mockito.mock(LinkedDataProofValidation.class); + utils.when(() -> { + LinkedDataProofValidation.newInstance(Mockito.any(DidResolver.class)); + }).thenReturn(mock); + Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); + Map status = revocationService.verifyStatus(credentialStatusDto); + Assertions.assertTrue(status.get(StringPool.STATUS).equals(CredentialStatus.ACTIVE.getName())); + } + } + + @SneakyThrows + @Test + void shouldVerifyStatusRevoke() { + + String indexTORevoke = "0"; + final var issuer = DID; + + //set bit at index + String encodedList = mockEmptyEncodedList(); + encodedList = BitSetManager.revokeCredential(encodedList, Integer.parseInt(indexTORevoke)); + + + var credentialBuilder = mockStatusListVC(issuer, "1", encodedList); + var statusListCredential = mockStatusListCredential(issuer, credentialBuilder); + // 1. create status list with the credential + var statusListIndex = mockStatusListIndex(issuer, statusListCredential, "0"); + when(statusListIndex.getStatusListCredential()).thenReturn(statusListCredential); + when(statusListCredentialRepository.findById(any(String.class))) + .thenReturn(Optional.of(statusListCredential)); + CredentialStatusDto credentialStatusDto = Mockito.mock(CredentialStatusDto.class); + when(credentialStatusDto.id()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1#0"); + when(credentialStatusDto.statusPurpose()).thenReturn("revocation"); + when(credentialStatusDto.statusListIndex()).thenReturn(indexTORevoke); + when(credentialStatusDto.statusListCredential()) + .thenReturn( + "http://this-is-my-domain/api/v1/revocations/credentials/" + + TestUtil.extractBpnFromDid(issuer) + + "/revocation/1"); + when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + try (MockedStatic utils = Mockito.mockStatic(LinkedDataProofValidation.class)) { + LinkedDataProofValidation mock = Mockito.mock(LinkedDataProofValidation.class); + utils.when(() -> { + LinkedDataProofValidation.newInstance(Mockito.any(DidResolver.class)); + }).thenReturn(mock); + Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); + Map status = revocationService.verifyStatus(credentialStatusDto); + + Assertions.assertTrue(status.get(StringPool.STATUS).equals(CredentialStatus.REVOKED.getName())); + } + } + } + + @Nested class RevokeTest { diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java new file mode 100644 index 00000000..80a83706 --- /dev/null +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java @@ -0,0 +1,33 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.revocation.services; + +import org.junit.jupiter.api.Test; + +public class StatusVerificationTest { + + + @Test + void testVerification() { + + } +} diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java index 5ee3fd5e..531b59a7 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java @@ -101,4 +101,6 @@ public class StringPool { public static final String CREDENTIAL_STATUS = "credentialStatus"; + public static final String STATUS = "status"; + } From 042292f1d4ebf6018721af1f748f3598ee619aa9 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 16:28:49 +0530 Subject: [PATCH 24/60] fix: dockerfile and dockerfile location --- .github/workflows/release.yml | 3 ++- miw/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4d31cbf..41ec6790 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,10 +171,11 @@ jobs: - name: Push image uses: docker/build-push-action@v5 with: - context: ./miw + context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + file: ./miw/Dockerfile # https://github.com/peter-evans/dockerhub-description # Important step to push image description to DockerHub diff --git a/miw/Dockerfile b/miw/Dockerfile index c92d9245..d0860a6f 100644 --- a/miw/Dockerfile +++ b/miw/Dockerfile @@ -27,7 +27,7 @@ RUN apk add curl USER miw -COPY ../LICENSE ../NOTICE.md ../DEPENDENCIES ../SECURITY.md ./build/libs/miw-latest.jar /app/ +COPY LICENSE NOTICE.md DEPENDENCIES SECURITY.md ./miw/build/libs/miw-latest.jar /app/ WORKDIR /app From 6a7cff2acb846fa9b664b359ec8fc179673df459 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 19:06:06 +0530 Subject: [PATCH 25/60] feat: test coverage verification added at root gradle level and javadoc for miw-commons --- build.gradle | 77 ++++++++++- miw/build.gradle | 63 --------- revocation-service/build.gradle | 55 -------- wallet-commons/build.gradle | 1 - .../commons/constant/ApplicationRole.java | 7 +- .../commons/constant/CredentialStatus.java | 12 +- .../commons/constant/RevocationPurpose.java | 15 ++- .../commons/constant/StringPool.java | 120 ++++++++++++++++++ .../commons/constant/SupportedAlgorithms.java | 9 ++ .../exception/BadDataExceptionTest.java | 56 ++++++++ .../exception/ForbiddenExceptionTest.java | 58 +++++++++ 11 files changed, 341 insertions(+), 132 deletions(-) create mode 100644 wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataExceptionTest.java create mode 100644 wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenExceptionTest.java diff --git a/build.gradle b/build.gradle index a0a9fac8..dfa23d53 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,8 @@ subprojects { plugin "java" plugin "org.springframework.boot" plugin "io.spring.dependency-management" - plugin 'jacoco-report-aggregation' + plugin "jacoco" + plugin 'project-report' } @@ -81,6 +82,68 @@ subprojects { enabled = false } + + test { + useJUnitPlatform() + finalizedBy jacocoTestReport + } + + jacocoTestReport { + + reports { + xml.enabled true + xml.outputLocation = file("./build/reports/xml/jacoco.xml") + + csv.enabled false + + html.enabled true + html.outputLocation = file("./build/reports/html/jacoco") + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "org/eclipse/tractusx/managedidentitywallets/dto/*", + "org/eclipse/tractusx/managedidentitywallets/dao/entity/*", + "org/eclipse/tractusx/managedidentitywallets/constant/*", + "org/eclipse/tractusx/managedidentitywallets/commons/constant/*", + "org/eclipse/tractusx/managedidentitywallets/exception/*" + ]) + })) + } + } + + jacoco { + toolVersion = "${jacocoVersion}" + } + + + jacocoTestCoverageVerification { + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + "org/eclipse/tractusx/managedidentitywallets/dto/*", + "org/eclipse/tractusx/managedidentitywallets/dao/entity/*", + "org/eclipse/tractusx/managedidentitywallets/constant/*", + "org/eclipse/tractusx/managedidentitywallets/commons/constant/*", + "org/eclipse/tractusx/managedidentitywallets/exception/*" + ]) + })) + } + violationRules { + rule { + limit { + minimum = 0.80 + } + } + } + } + + htmlDependencyReport { + projects = project.allprojects + } + + check.dependsOn jacocoTestCoverageVerification } @@ -116,8 +179,8 @@ tasks.register('dashDependencies') { dashDependencies -> conf.incoming.resolutionResult.allDependencies .findAll({ it instanceof ResolvedDependencyResult }) .collect { ResolvedDependencyResult dep -> - "${dep.selected}" - } + "${dep.selected}" + } ) } } @@ -126,10 +189,10 @@ tasks.register('dashDependencies') { dashDependencies -> def finalDeps = [] for (final def d in deps) { //skip main module dependencies - if(d.toString() =="project :miw" || d.toString() =="project :revocation-service"){ - println(" - "+d.toString() + " -") + if (d.toString() == "project :miw" || d.toString() == "project :revocation-service") { + println(" - " + d.toString() + " -") - }else{ + } else { finalDeps.add(d) } } @@ -148,7 +211,7 @@ tasks.register('dashLicenseCheck', JavaExec) { dashLicenseCheck -> doFirst { classpath = rootProject.files('dash.jar') // docs: https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-04 - args('-project', 'automotive.tractusx', '-summary', 'DEPENDENCIES', 'deps.txt') + args('-project', 'automotive.tractusx', '-summary', 'DEPENDENCIES', 'deps.txt') } doLast { logger.lifecycle("Removing 'deps.txt' now.") diff --git a/miw/build.gradle b/miw/build.gradle index 9b70d146..5b3c6c45 100644 --- a/miw/build.gradle +++ b/miw/build.gradle @@ -19,8 +19,6 @@ plugins { id 'java' - id "jacoco" - id 'project-report' id "de.undercouch.download" version "5.5.0" } @@ -94,64 +92,3 @@ bootJar { from '../LICENSE' } } - -test { - useJUnitPlatform() - finalizedBy jacocoTestReport -} - -htmlDependencyReport { - projects = project.allprojects -} - -jacocoTestReport { - - reports { - xml.enabled true - xml.outputLocation = file("./build/reports/xml/jacoco.xml") - - csv.enabled false - - html.enabled true - html.outputLocation = file("./build/reports/html/jacoco") - } - - afterEvaluate { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - "org/eclipse/tractusx/managedidentitywallets/dto/*", - "org/eclipse/tractusx/managedidentitywallets/dao/entity/*", - "org/eclipse/tractusx/managedidentitywallets/constant/*", - "org/eclipse/tractusx/managedidentitywallets/exception/*" - ]) - })) - } -} - -jacoco { - toolVersion = "${jacocoVersion}" -} - - -jacocoTestCoverageVerification { - afterEvaluate { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - "org/eclipse/tractusx/managedidentitywallets/dto/*", - "org/eclipse/tractusx/managedidentitywallets/dao/entity/*", - "org/eclipse/tractusx/managedidentitywallets/constant/*", - "org/eclipse/tractusx/managedidentitywallets/exception/*" - ]) - })) - } - violationRules { - rule { - limit { - // disabled for now - minimum = 0.00 - } - } - } -} - -check.dependsOn jacocoTestCoverageVerification diff --git a/revocation-service/build.gradle b/revocation-service/build.gradle index edb64565..477bbefc 100644 --- a/revocation-service/build.gradle +++ b/revocation-service/build.gradle @@ -19,8 +19,6 @@ plugins { id 'java' - id 'jacoco' - id 'project-report' } group = "${groupName}" @@ -81,56 +79,3 @@ build { version = "latest" } - -tasks.named('test') { - useJUnitPlatform() -} - -tasks.test { - finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run - testLogging { - events("passed", "skipped", "failed") - } -} -tasks.jacocoTestReport { - dependsOn(tasks.test) - reports { - xml.required = true - csv.required = false - html.required = true - } - - afterEvaluate { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - "org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.class", - "org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.class" - ]) - })) - } -} - -htmlDependencyReport { - projects = project.allprojects -} - - -jacocoTestCoverageVerification { - afterEvaluate { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - "org/eclipse/tractusx/managedidentitywallets/revocation/utils/StringPool.class", - "org/eclipse/tractusx/managedidentitywallets/revocation/config/security/SecurityConfigProperties.class" - ]) - })) - } - violationRules { - rule { - limit { - minimum = 0.8 - } - } - } -} - -check.dependsOn jacocoTestCoverageVerification diff --git a/wallet-commons/build.gradle b/wallet-commons/build.gradle index 45119542..87f366a4 100644 --- a/wallet-commons/build.gradle +++ b/wallet-commons/build.gradle @@ -19,7 +19,6 @@ plugins { id 'java-library' - id 'jacoco' id 'maven-publish' } diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java index 581d285a..9ef1870d 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/ApplicationRole.java @@ -21,11 +21,10 @@ package org.eclipse.tractusx.managedidentitywallets.commons.constant; -public class ApplicationRole { +import lombok.experimental.UtilityClass; - private ApplicationRole() { - throw new IllegalStateException("Constant class"); - } +@UtilityClass +public class ApplicationRole { /** * The constant ROLE_VIEW_WALLETS. diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java index ea5503f9..faa46f84 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/CredentialStatus.java @@ -23,10 +23,20 @@ import lombok.Getter; +/** + * The enum Credential status. + */ @Getter public enum CredentialStatus { - ACTIVE("active"), REVOKED("revoked"); + /** + * Active credential status. + */ + ACTIVE("active"), + /** + * Revoked credential status. + */ + REVOKED("revoked"); private final String name; diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java index 608ec2f5..794de68b 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/RevocationPurpose.java @@ -21,8 +21,21 @@ package org.eclipse.tractusx.managedidentitywallets.commons.constant; +import lombok.Getter; + +/** + * The enum Revocation purpose. + */ +@Getter public enum RevocationPurpose { - REVOCATION("revocation"), SUSPENSION("suspension"); + /** + * revocation purpose. + */ + REVOCATION("revocation"), + /** + * Suspension purpose. + */ + SUSPENSION("suspension"); private final String name; diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java index 531b59a7..d73feea1 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/StringPool.java @@ -23,25 +23,70 @@ import lombok.experimental.UtilityClass; +/** + * The type String pool. + */ @UtilityClass public class StringPool { + /** + * The constant CREDENTIAL_ID. + */ public static final String CREDENTIAL_ID = "credentialId"; + /** + * The constant VERIFIABLE_CREDENTIALS. + */ public static final String VERIFIABLE_CREDENTIALS = "verifiableCredentials"; + /** + * The constant VP. + */ public static final String VP = "vp"; + /** + * The constant VC. + */ public static final String VC = "vc"; + /** + * The constant VALID. + */ public static final String VALID = "valid"; + /** + * The constant VALIDATE_AUDIENCE. + */ public static final String VALIDATE_AUDIENCE = "validateAudience"; + /** + * The constant VALIDATE_EXPIRY_DATE. + */ public static final String VALIDATE_EXPIRY_DATE = "validateExpiryDate"; + /** + * The constant VALIDATE_JWT_EXPIRY_DATE. + */ public static final String VALIDATE_JWT_EXPIRY_DATE = "validateJWTExpiryDate"; + /** + * The constant DID_DOCUMENT. + */ public static final String DID_DOCUMENT = "didDocument"; + /** + * The constant ISSUER_DID. + */ public static final String ISSUER_DID = "issuerDid"; + /** + * The constant HOLDER_DID. + */ public static final String HOLDER_DID = "holderDid"; + /** + * The constant HOLDER_IDENTIFIER. + */ public static final String HOLDER_IDENTIFIER = "holderIdentifier"; + /** + * The constant TYPE. + */ public static final String TYPE = "type"; + /** + * The constant ED_25519. + */ public static final String ED_25519 = "ED25519"; @@ -55,38 +100,104 @@ public class StringPool { */ public static final String BPN = "bpn"; + /** + * The constant ID. + */ public static final String ID = "id"; + /** + * The constant CLIENT_ID. + */ public static final String CLIENT_ID = "miw_private_client"; + /** + * The constant CLIENT_SECRET. + */ public static final String CLIENT_SECRET = "miw_private_client_secret"; + /** + * The constant REALM. + */ public static final String REALM = "miw_test"; + /** + * The constant VALID_USER_NAME. + */ public static final String VALID_USER_NAME = "valid_user"; + /** + * The constant INVALID_USER_NAME. + */ public static final String INVALID_USER_NAME = "invalid_user"; + /** + * The constant CLIENT_CREDENTIALS. + */ public static final String CLIENT_CREDENTIALS = "client_credentials"; + /** + * The constant OPENID. + */ public static final String OPENID = "openid"; + /** + * The constant BEARER_SPACE. + */ public static final String BEARER_SPACE = "Bearer "; + /** + * The constant BPN_NUMBER_REGEX. + */ public static final String BPN_NUMBER_REGEX = "^(BPN)(L|S|A)[0-9A-Z]{12}"; + /** + * The constant W3_ID_JWS_2020_V1_CONTEXT_URL. + */ public static final String W3_ID_JWS_2020_V1_CONTEXT_URL = "https://w3id.org/security/suites/jws-2020/v1"; + /** + * The constant COMA_SEPARATOR. + */ public static final String COMA_SEPARATOR = ", "; + /** + * The constant BLANK_SEPARATOR. + */ public static final String BLANK_SEPARATOR = " "; + /** + * The constant COLON_SEPARATOR. + */ public static final String COLON_SEPARATOR = ":"; + /** + * The constant UNDERSCORE. + */ public static final String UNDERSCORE = "_"; + /** + * The constant REFERENCE_KEY. + */ public static final String REFERENCE_KEY = "dummy ref key, removed once vault setup is ready"; + /** + * The constant VAULT_ACCESS_TOKEN. + */ public static final String VAULT_ACCESS_TOKEN = "dummy vault access token, removed once vault setup is ready"; + /** + * The constant PRIVATE_KEY. + */ public static final String PRIVATE_KEY = "PRIVATE KEY"; + /** + * The constant PUBLIC_KEY. + */ public static final String PUBLIC_KEY = "PUBLIC KEY"; + /** + * The constant VC_JWT_KEY. + */ public static final String VC_JWT_KEY = "jwt"; + /** + * The constant AS_JWT. + */ public static final String AS_JWT = "asJwt"; + /** + * The constant BPN_CREDENTIAL. + */ public static final String BPN_CREDENTIAL = "BpnCredential"; public static final String ASSERTION_METHOD = "assertionMethod"; @@ -97,10 +208,19 @@ public class StringPool { public static final String HTTPS_SCHEME = "https://"; public static final String BPN_NOT_FOUND = "BPN not found"; + /** + * The constant REVOCABLE. + */ public static final String REVOCABLE = "revocable"; + /** + * The constant CREDENTIAL_STATUS. + */ public static final String CREDENTIAL_STATUS = "credentialStatus"; + /** + * The constant STATUS. + */ public static final String STATUS = "status"; } diff --git a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java index b9750cfe..adc2295a 100644 --- a/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/constant/SupportedAlgorithms.java @@ -21,9 +21,18 @@ package org.eclipse.tractusx.managedidentitywallets.commons.constant; +/** + * The enum Supported algorithms. + */ public enum SupportedAlgorithms { + /** + * Ed 25519 supported algorithms. + */ ED25519("ED25519"), + /** + * Es 256 k supported algorithms. + */ ES256K("ES256K"); private final String value; diff --git a/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataExceptionTest.java b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataExceptionTest.java new file mode 100644 index 00000000..feb1ba5a --- /dev/null +++ b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/BadDataExceptionTest.java @@ -0,0 +1,56 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.commons.exception; + + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class BadDataExceptionTest { + + public static final String ERROR_MSG = "This is a bad data exception"; + + @Test + void testConstructorWithMessage() { + String message = ERROR_MSG; + BadDataException exception = new BadDataException(message); + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertNull(exception.getCause()); + } + + @Test + void testConstructorWithMessageAndCause() { + String message = ERROR_MSG; + Throwable cause = new IllegalArgumentException("Invalid argument"); + BadDataException exception = new BadDataException(message, cause); + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(cause, exception.getCause()); + } + + @Test + void testConstructorWithCause() { + Throwable cause = new IllegalArgumentException("Invalid argument"); + BadDataException exception = new BadDataException(cause); + Assertions.assertEquals(cause.toString(), exception.getMessage()); + Assertions.assertEquals(cause, exception.getCause()); + } +} diff --git a/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenExceptionTest.java b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenExceptionTest.java new file mode 100644 index 00000000..4dcfd20a --- /dev/null +++ b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/exception/ForbiddenExceptionTest.java @@ -0,0 +1,58 @@ +/* + * ******************************************************************************* + * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ****************************************************************************** + */ + +package org.eclipse.tractusx.managedidentitywallets.commons.exception; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ForbiddenExceptionTest { + + @Test + void testConstructorWithMessage() { + String message = "Forbidden access!"; + ForbiddenException exception = new ForbiddenException(message); + Assertions.assertEquals(message, exception.getMessage()); + } + + @Test + void testConstructorWithMessageAndCause() { + String message = "Forbidden access!"; + Throwable cause = new IllegalArgumentException("Illegal argument"); + ForbiddenException exception = new ForbiddenException(message, cause); + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(cause, exception.getCause()); + } + + @Test + void testConstructorWithCause() { + Throwable cause = new IllegalArgumentException("Illegal argument"); + ForbiddenException exception = new ForbiddenException(cause); + Assertions.assertEquals(cause, exception.getCause()); + } + + @Test + void testDefaultConstructor() { + ForbiddenException exception = new ForbiddenException(); + Assertions.assertNull(exception.getMessage()); + Assertions.assertNull(exception.getCause()); + } +} From 168493aa90319611188b99a8be6f85568931537c Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 19:10:15 +0530 Subject: [PATCH 26/60] doc: env updated for MIW --- miw/README.md | 64 ++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/miw/README.md b/miw/README.md index 25f6e815..0ea651ac 100644 --- a/miw/README.md +++ b/miw/README.md @@ -160,7 +160,8 @@ Description of the env files: - **env.local**: Set up everything to get ready for flow "local". You need to fill in the passwords. - **env.docker**: Set up everything to get ready for flow "docker". You need to fill in the passwords. -> **IMPORTANT**: ssi-lib is resolving DID documents over the network. There are two endpoints that rely on this resolution: +> **IMPORTANT**: ssi-lib is resolving DID documents over the network. There are two endpoints that rely on this +> resolution: > - Verifiable Credentials - Validation > - Verifiable Presentations - Validation > @@ -286,36 +287,37 @@ This process ensures that any issues with the database schema are resolved by re # Environment Variables -| name | description | default value | -|---------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| APPLICATION_PORT | port number of application | 8080 | -| APPLICATION_ENVIRONMENT | Environment of the application ie. local, dev, int and prod | local | -| DB_HOST | Database host | localhost | -| DB_PORT | Port of database | 5432 | -| DB_NAME | Database name | miw | -| USE_SSL | Whether SSL is enabled in database server | false | -| DB_USER_NAME | Database username | | -| DB_PASSWORD | Database password | | -| DB_POOL_SIZE | Max number of database connection acquired by application | 10 | -| KEYCLOAK_MIW_PUBLIC_CLIENT | Only needed if we want enable login with keyalock in swagger | miw_public | -| MANAGEMENT_PORT | Spring actuator port | 8090 | -| MIW_HOST_NAME | Application host name, this will be used in creation of did ie. did:web:MIW_HOST_NAME:BPN | localhost | -| ENCRYPTION_KEY | encryption key used to encrypt and decrypt private and public key of wallet | | -| AUTHORITY_WALLET_BPN | base wallet BPN number | BPNL000000000000 | -| AUTHORITY_WALLET_NAME | Base wallet name | Catena-X | -| AUTHORITY_WALLET_DID | Base wallet web did | web:did:host:BPNL000000000000 | -| VC_SCHEMA_LINK | Comma separated list of VC schema URL | https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json | -| VC_EXPIRY_DATE | Expiry date of VC (dd-MM-yyyy ie. 01-01-2025 expiry date will be 2024-12-31T18:30:00Z in VC) | 01-01-2025 | -| KEYCLOAK_REALM | Realm name of keycloak | miw_test | -| KEYCLOAK_CLIENT_ID | Keycloak private client id | | -| AUTH_SERVER_URL | Keycloak server url | | -| SUPPORTED_FRAMEWORK_VC_TYPES | Supported framework VC, provide values ie type1=value1,type2=value2 | cx-behavior-twin=Behavior Twin,cx-pcf=PCF,cx-quality=Quality,cx-resiliency=Resiliency,cx-sustainability=Sustainability,cx-traceability=ID_3.0_Trace | -| ENFORCE_HTTPS_IN_DID_RESOLUTION | Enforce https during web did resolution | true | -| CONTRACT_TEMPLATES_URL | Contract templates URL used in summary VC | https://public.catena-x.org/contracts/ | -| APP_LOG_LEVEL | Log level of application | INFO | -| AUTHORITY_SIGNING_SERVICE_TYPE | Base wallet signing type, Currency only LOCAL is supported | Local | -| LOCAL_SIGNING_KEY_STORAGE_TYPE | Key storage type, currently only DB is supported | DB | -| | | | +| name | description | default value | +|-----------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| APPLICATION_PORT | port number of application | 8080 | +| APPLICATION_ENVIRONMENT | Environment of the application ie. local, dev, int and prod | local | +| DB_HOST | Database host | localhost | +| DB_PORT | Port of database | 5432 | +| DB_NAME | Database name | miw | +| USE_SSL | Whether SSL is enabled in database server | false | +| DB_USER_NAME | Database username | | +| DB_PASSWORD | Database password | | +| DB_POOL_SIZE | Max number of database connection acquired by application | 10 | +| KEYCLOAK_MIW_PUBLIC_CLIENT | Only needed if we want enable login with keyalock in swagger | miw_public | +| MANAGEMENT_PORT | Spring actuator port | 8090 | +| MIW_HOST_NAME | Application host name, this will be used in creation of did ie. did:web:MIW_HOST_NAME:BPN | localhost | +| ENCRYPTION_KEY | encryption key used to encrypt and decrypt private and public key of wallet | | +| AUTHORITY_WALLET_BPN | base wallet BPN number | BPNL000000000000 | +| AUTHORITY_WALLET_NAME | Base wallet name | Catena-X | +| AUTHORITY_WALLET_DID | Base wallet web did | web:did:host:BPNL000000000000 | +| VC_SCHEMA_LINK | Comma separated list of VC schema URL | https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json | +| VC_EXPIRY_DATE | Expiry date of VC (dd-MM-yyyy ie. 01-01-2025 expiry date will be 2024-12-31T18:30:00Z in VC) | 01-01-2025 | +| KEYCLOAK_REALM | Realm name of keycloak | miw_test | +| KEYCLOAK_CLIENT_ID | Keycloak private client id | | +| AUTH_SERVER_URL | Keycloak server url | | +| SUPPORTED_FRAMEWORK_VC_TYPES | Supported framework VC, provide values ie type1=value1,type2=value2 | cx-behavior-twin=Behavior Twin,cx-pcf=PCF,cx-quality=Quality,cx-resiliency=Resiliency,cx-sustainability=Sustainability,cx-traceability=ID_3.0_Trace | +| ENFORCE_HTTPS_IN_DID_RESOLUTION | Enforce https during web did resolution | true | +| CONTRACT_TEMPLATES_URL | Contract templates URL used in summary VC | https://public.catena-x.org/contracts/ | +| APP_LOG_LEVEL | Log level of application | INFO | +| AUTHORITY_SIGNING_SERVICE_TYPE | Base wallet signing type, Currency only LOCAL is supported | Local | +| LOCAL_SIGNING_KEY_STORAGE_TYPE | Key storage type, currently only DB is supported | DB | +| BITSTRING_STATUS_LIST_CONTEXT_URL | Context URI for bitstring status list | https://w3c.github.io/vc-bitstring-status-list/contexts/v1.jsonld | +| | | | # Technical Debts and Known issue From 107538cc8bcb59ff2b95f787a63329862a020a92 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 20:27:43 +0530 Subject: [PATCH 27/60] doc: docs added for revocation service --- docs/api/revocation-service/openapi_v001.json | 409 ++++++++++-------- .../images/SingleInstanceDomainView.png | Bin docs/arc42/{ => miw}/images/VPP-Flow.png | Bin docs/arc42/{ => miw}/images/VVP-Flow.puml | 0 .../{ => miw}/images/business_context.png | Bin .../{ => miw}/images/issue_revocable_vc.png | Bin .../{ => miw}/images/technical_context.png | Bin .../{ => miw}/images/verify_revocable_vc.png | Bin docs/arc42/{ => miw}/main.md | 6 +- .../images/business_context.png | Bin 0 -> 255837 bytes .../images/get_status_list_entry_api.png | Bin 0 -> 6537 bytes .../images/get_status_list_vc.png | Bin 0 -> 7005 bytes .../images/issue_revocable_vc.png | Bin 0 -> 43766 bytes .../images/revocation-tech_context.png | Bin 0 -> 38707 bytes .../revocation-service/images/revoke_vc.png | Bin 0 -> 6054 bytes .../images/technical_context.png | Bin 0 -> 280380 bytes .../images/verify_revocable_vc.png | Bin 0 -> 36718 bytes .../images/verify_status.png | Bin 0 -> 7252 bytes docs/arc42/revocation-service/main.md | 405 +++++++++++++++++ docs/{ => database/miw}/ArchitectureWebDID.md | 0 .../miw}/MIW_DB_Schema_v0.0.1.md | 0 .../revocation/revocation_DB_Schema_v0.0.1.md | 24 + .../security-assessment-23-12.md | 60 +-- .../RevocationApiControllerApiDocs.java | 65 ++- 24 files changed, 743 insertions(+), 226 deletions(-) rename docs/arc42/{ => miw}/images/SingleInstanceDomainView.png (100%) rename docs/arc42/{ => miw}/images/VPP-Flow.png (100%) rename docs/arc42/{ => miw}/images/VVP-Flow.puml (100%) rename docs/arc42/{ => miw}/images/business_context.png (100%) rename docs/arc42/{ => miw}/images/issue_revocable_vc.png (100%) rename docs/arc42/{ => miw}/images/technical_context.png (100%) rename docs/arc42/{ => miw}/images/verify_revocable_vc.png (100%) rename docs/arc42/{ => miw}/main.md (99%) create mode 100644 docs/arc42/revocation-service/images/business_context.png create mode 100644 docs/arc42/revocation-service/images/get_status_list_entry_api.png create mode 100644 docs/arc42/revocation-service/images/get_status_list_vc.png create mode 100644 docs/arc42/revocation-service/images/issue_revocable_vc.png create mode 100644 docs/arc42/revocation-service/images/revocation-tech_context.png create mode 100644 docs/arc42/revocation-service/images/revoke_vc.png create mode 100644 docs/arc42/revocation-service/images/technical_context.png create mode 100644 docs/arc42/revocation-service/images/verify_revocable_vc.png create mode 100644 docs/arc42/revocation-service/images/verify_status.png create mode 100644 docs/arc42/revocation-service/main.md rename docs/{ => database/miw}/ArchitectureWebDID.md (100%) rename docs/{ => database/miw}/MIW_DB_Schema_v0.0.1.md (100%) create mode 100644 docs/database/revocation/revocation_DB_Schema_v0.0.1.md diff --git a/docs/api/revocation-service/openapi_v001.json b/docs/api/revocation-service/openapi_v001.json index ba06ff8d..0886659c 100644 --- a/docs/api/revocation-service/openapi_v001.json +++ b/docs/api/revocation-service/openapi_v001.json @@ -1,131 +1,178 @@ { - "openapi": "3.0.1", - "info": { - "title": "Reovcation Service API", - "description": "Revocation Service API", - "contact": { - "name": "eclipse-tractusx", - "url": "https://projects.eclipse.org/projects/automotive.tractusx", - "email": "tractusx-dev@eclipse.org" + "openapi" : "3.0.1", + "info" : { + "title" : "Reovcation Service API", + "description" : "Revocation Service API", + "contact" : { + "name" : "eclipse-tractusx", + "url" : "https://projects.eclipse.org/projects/automotive.tractusx", + "email" : "tractusx-dev@eclipse.org" }, - "version": "0.0.1" + "version" : "0.0.1" }, - "servers": [ + "servers" : [ { - "url": "http://localhost:8080", - "description": "Generated server url" + "url" : "http://localhost:8081", + "description" : "Generated server url" } ], - "security": [ + "security" : [ { - "Authenticate using access_token": [] + "Authenticate using access_token" : [] } ], - "tags": [ + "tags" : [ { - "name": "Revocation Service", - "description": "Revocation Service API" + "name" : "Revocation Service", + "description" : "Revocation Service API" } ], - "paths": { - "/api/v1/revocations/status-entry": { - "post": { - "tags": [ + "paths" : { + "/api/v1/revocations/verify" : { + "post" : { + "tags" : [ "Revocation Service" ], - "summary": "Create or Update a Status List Credential", - "description": "Create the status list credential if it does not exist, else update it.", - "operationId": "createStatusListVC", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StatusEntryDto" + "summary" : "Verify Revocation status", + "description" : "Verify revocation status of Credential", + "operationId" : "verifyRevocation", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredentialStatusDto" }, - "example": { - "purpose": "revocation", - "issuerId": "did:web:localhost:BPNL000000000000" + "example" : { + "id" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1#12", + "statusPurpose" : "revocation", + "statusListIndex" : "12", + "statusListCredential" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type" : "BitstringStatusListEntry" } } }, - "required": true + "required" : true }, - "responses": { - "403": { - "description": "ForbiddenException: invalid caller" + "responses" : { + "401" : { + "description" : "UnauthorizedException: invalid token" }, - "200": { - "description": "Status list credential created/updated successfully.", - "content": { - "application/json": { - "example": { - "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0", - "statusPurpose": "revocation", - "statusListIndex": "0", - "statusListCredential": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" - } + "200" : { + "description" : "if credential is revoked" + }, + "500" : { + "description" : "RevocationServiceException: Internal Server Error" + }, + "409" : { + "description" : "ConflictException: Revocation service error", + "content" : { + "application/json" : {} + } + }, + "403" : { + "description" : "ForbiddenException: invalid caller" + } + } + } + }, + "/api/v1/revocations/status-entry" : { + "post" : { + "tags" : [ + "Revocation Service" + ], + "summary" : "Create or Update a Status List Credential", + "description" : "Create the status list credential if it does not exist, else update it.", + "operationId" : "createStatusListVC", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/StatusEntryDto" + }, + "example" : { + "purpose" : "revocation", + "issuerId" : "did:web:localhost:BPNL000000000000" } } }, - "401": { - "description": "UnauthorizedException: invalid token" + "required" : true + }, + "responses" : { + "401" : { + "description" : "UnauthorizedException: invalid token" + }, + "500" : { + "description" : "RevocationServiceException: Internal Server Error" + }, + "403" : { + "description" : "ForbiddenException: invalid caller" }, - "500": { - "description": "RevocationServiceException: Internal Server Error" + "200" : { + "description" : "Status list credential created/updated successfully.", + "content" : { + "application/json" : { + "example" : { + "id" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1#17", + "statusPurpose" : "revocation", + "statusListIndex" : "17", + "statusListCredential" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type" : "BitstringStatusListEntry" + } + } + } } } } }, - "/api/v1/revocations/revoke": { - "post": { - "tags": [ + "/api/v1/revocations/revoke" : { + "post" : { + "tags" : [ "Revocation Service" ], - "summary": "Revoke a VerifiableCredential", - "description": "Revoke a VerifiableCredential using the provided Credential Status", - "operationId": "revokeCredential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusDto" + "summary" : "Revoke a VerifiableCredential", + "description" : "Revoke a VerifiableCredential using the provided Credential Status", + "operationId" : "revokeCredential", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/CredentialStatusDto" }, - "example": { - "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1#0", - "statusPurpose": "revocation", - "statusListIndex": "0", - "statusListCredential": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "example" : { + "id" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1#12", + "statusPurpose" : "revocation", + "statusListIndex" : "12", + "statusListCredential" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type" : "BitstringStatusListEntry" } } }, - "required": true + "required" : true }, - "responses": { - "403": { - "description": "ForbiddenException: invalid caller" + "responses" : { + "200" : { + "description" : "Verifiable credential revoked successfully." }, - "200": { - "description": "Verifiable credential revoked successfully." + "401" : { + "description" : "UnauthorizedException: invalid token" }, - "401": { - "description": "UnauthorizedException: invalid token" + "500" : { + "description" : "RevocationServiceException: Internal Server Error" }, - "500": { - "description": "RevocationServiceException: Internal Server Error" + "403" : { + "description" : "ForbiddenException: invalid caller" }, - "409": { - "description": "ConflictException: Revocation service error", - "content": { - "application/json": { - "example": { - "type": "BitstringStatusListEntry", - "title": "Revocation service error", - "status": 409, - "detail": "Credential already revoked", - "instance": "/api/v1/revocations/revoke", - "timestamp": 1707133388128 + "409" : { + "description" : "ConflictException: Revocation service error", + "content" : { + "application/json" : { + "example" : { + "type" : "BitstringStatusListEntry", + "title" : "Revocation service error", + "status" : "409", + "detail" : "Credential already revoked", + "instance" : "/api/v1/revocations/revoke", + "timestamp" : 1707133388128 } } } @@ -133,144 +180,144 @@ } } }, - "/api/v1/revocations/credentials/{issuerBPN}/{status}/{index}": { - "get": { - "tags": [ + "/api/v1/revocations/credentials/{issuerBPN}/{status}/{index}" : { + "get" : { + "tags" : [ "Revocation Service" ], - "summary": "Get status list credential", - "description": "Get status list credential using the provided issuer BPN and status purpose and status list index", - "operationId": "getStatusListCredential", - "parameters": [ + "summary" : "Get status list credential", + "description" : "Get status list credential using the provided issuer BPN and status purpose and status list index", + "operationId" : "getStatusListCredential", + "parameters" : [ { - "name": "issuerBPN", - "in": "path", - "description": "Issuer BPN", - "required": true, - "schema": { - "type": "string" + "name" : "issuerBPN", + "in" : "path", + "description" : "Issuer BPN", + "required" : true, + "schema" : { + "type" : "string" }, - "example": "BPNL000000000000" + "example" : "BPNL000000000000" }, { - "name": "status", - "in": "path", - "description": "Status Purpose ( Revocation or Suspension)", - "required": true, - "schema": { - "type": "string" + "name" : "status", + "in" : "path", + "description" : "Status Purpose ( Revocation or Suspension)", + "required" : true, + "schema" : { + "type" : "string" }, - "example": "revocation" + "example" : "revocation" }, { - "name": "index", - "in": "path", - "description": "status list index", - "required": true, - "schema": { - "type": "string" + "name" : "index", + "in" : "path", + "description" : "status list index", + "required" : true, + "schema" : { + "type" : "string" }, - "example": 1 + "example" : 1 } ], - "responses": { - "404": { - "description": "Status list credential not found" - }, - "500": { - "description": "RevocationServiceException: Internal Server Error" + "responses" : { + "500" : { + "description" : "RevocationServiceException: Internal Server Error" }, - "200": { - "description": "Get Status list credential ", - "content": { - "application/json": { - "example": { - "@context": [ + "200" : { + "description" : "Get Status list credential ", + "content" : { + "application/json" : { + "example" : { + "@context" : [ "https://www.w3.org/2018/credentials/v1", "https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json", "https://w3id.org/security/suites/jws-2020/v1" ], - "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": [ + "id" : "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type" : [ "VerifiableCredential", "BitstringStatusListCredential" ], - "issuer": "did:web:localhost:BPNL000000000000", - "issuanceDate": "2024-02-05T09:39:58Z", - "credentialSubject": [ + "issuer" : "did:web:localhost:BPNL000000000000", + "issuanceDate" : "2024-02-05T09:39:58Z", + "credentialSubject" : [ { - "statusPurpose": "revocation", - "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusList", - "encodedList": "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" + "statusPurpose" : "revocation", + "id" : "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type" : "BitstringStatusList", + "encodedList" : "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" } ], - "proof": { - "proofPurpose": "assertionMethod", - "type": "JsonWebSignature2020", - "verificationMethod": "did:web:localhost:BPNL000000000000#ed463e4c-b900-481a-b5d0-9ae439c434ae", - "created": "2024-02-05T09:39:58Z", - "jws": "eyJhbGciOiJFZERTQSJ9..swX1PLJkSlxB6JMmY4a2uUzR-uszlyLrVdNppoYSx4PTV1LzQrDb0afzp_dvTNUWEYDI57a8iPh78BDjqMjSDQ" + "proof" : { + "proofPurpose" : "assertionMethod", + "type" : "JsonWebSignature2020", + "verificationMethod" : "did:web:localhost:BPNL000000000000#ed463e4c-b900-481a-b5d0-9ae439c434ae", + "created" : "2024-02-05T09:39:58Z", + "jws" : "eyJhbGciOiJFZERTQSJ9..swX1PLJkSlxB6JMmY4a2uUzR-uszlyLrVdNppoYSx4PTV1LzQrDb0afzp_dvTNUWEYDI57a8iPh78BDjqMjSDQ" } } } } + }, + "404" : { + "description" : "Status list credential not found" } } } } }, - "components": { - "schemas": { - "StatusEntryDto": { - "required": [ - "issuerId", - "purpose" - ], - "type": "object", - "properties": { - "purpose": { - "type": "string" - }, - "issuerId": { - "type": "string" - } - } - }, - "CredentialStatusDto": { - "required": [ + "components" : { + "schemas" : { + "CredentialStatusDto" : { + "required" : [ "id", "statusListCredential", "statusListIndex", "statusPurpose", "type" ], - "type": "object", - "properties": { - "id": { - "type": "string" + "type" : "object", + "properties" : { + "id" : { + "type" : "string" + }, + "statusPurpose" : { + "type" : "string" }, - "statusPurpose": { - "type": "string" + "statusListIndex" : { + "type" : "string" }, - "statusListIndex": { - "type": "string" + "statusListCredential" : { + "type" : "string" }, - "statusListCredential": { - "type": "string" + "type" : { + "type" : "string" + } + } + }, + "StatusEntryDto" : { + "required" : [ + "issuerId", + "purpose" + ], + "type" : "object", + "properties" : { + "purpose" : { + "type" : "string" }, - "type": { - "type": "string" + "issuerId" : { + "type" : "string" } } } }, - "securitySchemes": { - "Authenticate using access_token": { - "type": "apiKey", - "description": "**Bearer (apiKey)**\nJWT Authorization header using the Bearer scheme.\nEnter **Bearer** [space] and then your token in the text input below.\nExample: Bearer 12345abcdef", - "name": "Authorization", - "in": "header" + "securitySchemes" : { + "Authenticate using access_token" : { + "type" : "apiKey", + "description" : "**Bearer (apiKey)**\nJWT Authorization header using the Bearer scheme.\nEnter **Bearer** [space] and then your token in the text input below.\nExample: Bearer 12345abcdef", + "name" : "Authorization", + "in" : "header" } } } diff --git a/docs/arc42/images/SingleInstanceDomainView.png b/docs/arc42/miw/images/SingleInstanceDomainView.png similarity index 100% rename from docs/arc42/images/SingleInstanceDomainView.png rename to docs/arc42/miw/images/SingleInstanceDomainView.png diff --git a/docs/arc42/images/VPP-Flow.png b/docs/arc42/miw/images/VPP-Flow.png similarity index 100% rename from docs/arc42/images/VPP-Flow.png rename to docs/arc42/miw/images/VPP-Flow.png diff --git a/docs/arc42/images/VVP-Flow.puml b/docs/arc42/miw/images/VVP-Flow.puml similarity index 100% rename from docs/arc42/images/VVP-Flow.puml rename to docs/arc42/miw/images/VVP-Flow.puml diff --git a/docs/arc42/images/business_context.png b/docs/arc42/miw/images/business_context.png similarity index 100% rename from docs/arc42/images/business_context.png rename to docs/arc42/miw/images/business_context.png diff --git a/docs/arc42/images/issue_revocable_vc.png b/docs/arc42/miw/images/issue_revocable_vc.png similarity index 100% rename from docs/arc42/images/issue_revocable_vc.png rename to docs/arc42/miw/images/issue_revocable_vc.png diff --git a/docs/arc42/images/technical_context.png b/docs/arc42/miw/images/technical_context.png similarity index 100% rename from docs/arc42/images/technical_context.png rename to docs/arc42/miw/images/technical_context.png diff --git a/docs/arc42/images/verify_revocable_vc.png b/docs/arc42/miw/images/verify_revocable_vc.png similarity index 100% rename from docs/arc42/images/verify_revocable_vc.png rename to docs/arc42/miw/images/verify_revocable_vc.png diff --git a/docs/arc42/main.md b/docs/arc42/miw/main.md similarity index 99% rename from docs/arc42/main.md rename to docs/arc42/miw/main.md index 67422f20..da8faeb3 100644 --- a/docs/arc42/main.md +++ b/docs/arc42/miw/main.md @@ -89,11 +89,11 @@ with other DID agents. ## Business Context -![](./images/business_context.png) +![](images/business_context.png) ## Technical Context -![](./images/technical_context.png) +![](images/technical_context.png) # Solution Strategy @@ -114,7 +114,7 @@ service using the following technology stack: This diagram is a zoomed out view of the MIW and the components it interacts with. -![](./images/SingleInstanceDomainView.png) +![](images/SingleInstanceDomainView.png) ## Overall System diff --git a/docs/arc42/revocation-service/images/business_context.png b/docs/arc42/revocation-service/images/business_context.png new file mode 100644 index 0000000000000000000000000000000000000000..61229f67028a0b5f5b9f59ded5a9636e80c9de9e GIT binary patch literal 255837 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�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 v)<4FlDd4IKFIOOifcLSY0A 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 Ik(1Z{503j8DgXcg literal 0 HcmV?d00001 diff --git a/docs/arc42/revocation-service/images/get_status_list_entry_api.png b/docs/arc42/revocation-service/images/get_status_list_entry_api.png new file mode 100644 index 0000000000000000000000000000000000000000..d135c4a580d3eb6d48b56ea19b24f0c52ebb265a GIT binary patch literal 6537 zcmb7pcRXBO_iuEfcY=sSNrE7RXd`MK(bIHBuR{b;MmKtpD1)exh$Mp1+i0Ubj37EQ zW0VMD7@aZ7CFT9S_xHK~+<*2tpS@Q(>#VcB`>gMdGt|FNfAP}A3l}cXYyYG1@WKT$ z*xCI(YRa?mT2Hptg$vw<+8U~l{jJt=<1(IThV&e_{t#Re^imdf%;Bx#l9tj>;g(*w z%bJ{|@GeQql#+y*$+OclB(> zjXE_f{lDbZGs(aFm-r=wAkg7|#K)P0Nksq0d}TCDjihOP?AGv}syMvs7v6v%(TJvpy#M_av2eo9uNj@E=_b=}3l@ zFp%N=9^3C!#5z*)36h`+I@fOed0bPR5*%APc@W2Q+=tAQ#}Dk=cr^x6{gE;)&w)>_ z1Eyi^+hY?wk2VV{CTl~>L!qkxEHs|=m(5)&wK0gBZcgot>L2IhqD1}6zAA?$P09(z zrTSe7-SdQRtVdNxOT^Ih|7gvO53OG0h&Gnhw;n8r6a@05)hm}cbbZjXid7<@KR&h) z=Z*~vo^8J$dL)gnG)1{Y*E7B8s_`mr6jGPd%m-mwr05Pt@jx(}G^P9}CPb&b@8A2-Dpytn9OHj|}UYQ4blxGLo*ED&r)<13xojUoP z9qO8Z8VwmL1)-L@`3~>K5tI2w4%0c+gXfQ2Rnd84TeF^_cq#(Vi=fx>B|`TH^1bYe zb@$ydbt3HiRkcN36RUGrFdmW5*{1P7Dt%N8=jRs#E#k4@?s} zq6m46)om%AY7|P>IxEAE6zNb}0ZSJo%hT=5umove3@;dehdniF~G)tx1p`w51R z#42s(l=I}3+%2Of7t2$n{5D2C>Dt%tmI1GK{UlsIlA-v}Mk;{t^FP+Ak^}vD`oESC(^y6Y(*Z_w$QersPI+ zr-H1%oX&5L)=0(b4d>w$^5c1~Q~wMNo}D=HoTg`dkt6*6&EeCI#Co~2!bxlglH=Zm z`Xi2)B~GuhTSaT~OYBah+~+kWSNP~cfty0+dSIhi*LR9&wI}D+TMl*a!f(?kh87{k zm21C(097ilKXJA+r1{&nHiV4D)L1txLwP}7Joe}(W^^V>AJ=)Oyj%17kf51)C2xRMEQs5Ln+RjCIAam_x+ql*HbbsC- zA7%IKpT^e<#r5T2)bwyE8a`D_dxIrbb{2lhnJj3-dt^T$VciO#aT;=%^EahdoSFJ@ zS|jw{f!zIRRyY6smhap7GTvpJSNGBT1qR4_lYMGR`q_1Dj2a+Vzpr&JU~Zx0lghJu zEH>YLmoTU6CmRUoy3^5$#~7~8=)riB<$emh@TSG~QD&TWZn#1VVTNk`u^-mk?BLr) zYmN!CODDB~Q8PE83(P6XD%&tY#&k>&x@2Sw1Ng`fcG^WXX;ne_Tz;yW;sdp#@Q87O52@loBP$Be1Y$&`sgmMO8B|A!_?XTFuXI7 zJD+mSc1~(8%=cS-`lo|JWfjEk?H_bxG~VddJAyp}m!lgXx5yidphHK?ZLm z(bAyk;%AEGX|VpX>#++|lCVX|eEE`Ui3yp^@o zPMi+hOG5=$-&r|Dlche4T` z5GC1r!d?cO)C#7zf1$se@x^qtLybhy7HzKr<`3N9B2Qs3s>Oeo!k1Roi@kP!Hp$|` zb+=o`b`!ze(}sl5T;@hpDup{xxLaj0G1N1&(j5R)V1wYd+;9+@fN5?%n%3p74Mai( z`<0v>ekL1_b~3GLHw~B`Sfym>F-TsyBh_;uL&o2nV47{XAxW)O@J577)fo%{4F!E! z$O+Y=RugraYV-xrsU;7EON#_?%}ABlp{<}QySAi};bj3=Ty%7t9GL4|_VukjX{9Az ziBnLng3*e*s75^U@zkq{I&ld|LDaX_j&S+4f0gHJu8EWRyCtZDjzyR@tjsL~*7tB? zp~K#MF>&Q;odCS+}Q--xN{pd%M zkZA)%02SRDE=3Abj?%evQAb(HZOcMP#~o)uuPCUCB?l{unWPN`>7`G-EUv%j-kC4e z-s(3|Gkx0jBb`QewpP`VPDi0Mr?+ZmhfH-E(b;cjz_`ECH<}*~kNRqP5b`iOH_2Tm zN6$z7x}&Xsi~sc9^endk=-ll=NzffIa9dxX_NZedlz9(Yx64V?wO)``n?N}8Bh&_Y z2r1B|OW>!8g)N{NQQ(E7x~Acj-ooP?C6zq6&7H&zo zvgi~C2>ci936k)I_sbfzZMAsufi$?OJ(U`~j&IUTo|iSfjj6Ji(lz0Q*>rC2ln~7m z8|?RLXYlxD^QPbOlw7j1=!3AG-!~%o-2hrNxP8kW9_w{@ zo3!?$E{sGjLLiBZl|Nnbo9TxWe-$w?w%1Ri>1AE4b&OqZh(mPJs;EcC)!L#XQo+?5 zLy?YP<27kAe?V37Fai;rIoBE6E^UK-Zcz?zp%=4B${=pZyO9bSyxbIHj`V7NITT4# z!vb^o>1lQRU}HT6z>pj#aw$HK^?Oa0(GH-?0cJ39Qnq6u8XjgR?|wHc{7SX*@Tdw0 z1Wr|peY2mqca}VtYpk$XAd1xhASO5P=J6*%pr|GPgq)y-CbJzO##{-f7~8r4UcfVs z2ThclywT+3F1wHfZ(7d{t;^|9ZD^26L$Gd08~NA^!{4qHI3l zruM!{lcUJQL{*r>kDPWU`!!43+8IhE{|tsa`IQV3GZQgj`<}-Z&npfNe%K?ewn})i zzmkSmj?K(N$|T-S1h%3MdHos?ndU;aUL2jM-9@ryzxXVS#VYZYA&_G%pQL7yJy5QH z+c824?bJ7ntk-9X*M>;9f{+(7!(mY|D{cb7_ATlt&f{*x$|IY`+X(8xn=_>wA}NUG zPQ%eU`a{vkXL~f$_K5+92%iAkmqL3_8R01ZpY;GYnp01s8;vK0YQHpOy3O3aD0a9PyDIb*URK4J7sVrsdQCmB70&R%po+#=$@ZvVLeDX z4mZWoCM*DP-(7lv-}c<15HdP#DI(wsoS$6y)S)EyDhaXxt)#P=*YN7uhC3O`pPNTy zwo>J`5?!>AFJw*rMg7m)36yARQcXqA2&BWRsv z^27iGy~4048pme4of)=Pv6$Dv>eWGnrDldZU7~G#r9|R(99HX~Ei*VXJw$#9;;<>N z!zGouZq7ikwCp$U4Tdao?1I{|Id3)4E5B|^s@~Iav(0)wf5LmYpo^%=g-?j;B9+A@ z%#mnoEjwt@#gIy~!Nx{a7=@W}3hUBvl|z=R;{fwU_>%qa;^M9q8S($lDDi$cjl_$3 z$McXGuTYq#B)^dmGJuU$C%+b)MwUnI^yyB%yg(KlHv>u_AB=_v>?s+~Xp@WKML=y3 z5MGrsB*{)ZH8MIW!A()@w`8VSJ$b=(X(bdX!wU zSFSR4?d{r%-pIJOl>D;GKEbZprG>R;_a+cz*f|^N;OnI%39KNwIN!-|ELlnJuIpdG z5_w}gKFURmE?ixKvL7N1d%4Pn2>Ys0+&+6tmo@J(4=Y0i5^5?l`$2!R^6N&yCCYi5 z>B2+rwg(oT*U;wupWfT#Eb`n^uMvt3tB*x8ARXj^2RS(IDZqWDf`GwcrQ1Jxdu`i? z7qtU8pdv3f0GnAM$SPWc-5OQ-v=tN(9G1mPjf#|2heKR8`(k{T6jo7lpUDv*?a$1# zlZLOR@4U^t?+n=#cpFyw;Y4$zO2$ThhaFry_xVD?AXs0^Dw|sqpK607(hJAW%Q(#D z#$gTi(qafx``oG8xTrdx>ThUHtlBj`~q#g_n| z?J~wyo~53jVH?UzGixxBt)&KYGsW+5XNY)BX(q1?tTpoCPRBHqP~C`@p+@Gf$KT#Qui ziDP0MN}7>)_C79mdkN5~sWUsz$ntc)?xr;CUyng5aC1c?kHp|C!B(%6>{lF$idqEb z@StYX5AAfTnB1tzQ5WR`(inwiI zmN3CBW$Q%;=vZRUNOXElq~GqO)wJ+8dBQPAQ$!cE5ZE;%LOaN&g^hRA(7leJKjgkT zymb>*x3=@02|^Wla#wsttkZ6}f*~|1>NxzZ$^Y1a9HTAoC zV!B9{rAMA;+q{0;$?mX|a>&XvEqHiLdOo5dbWKBUJ|Sv7PTU)V4VFP*Dqq;2`egSs zW*gJ{omRVk0e@Z;-^bMw)@Q~R0y|nTk9giY^j1oQPI-UYa@9lUr2}Ve)CzC~(LSbR zB9do#mOB_rqu-r4l1?IU&r0)hmf7=nefG2eG*w_n_W~K%D7(e<=%UOxs7)z{(o9zZ zm0=#;ca36=FTZGkHL&aF+WS=@jxzN#4k8_xb@Ot;Gnyq}ECmkOwt64Kq~L{4w7uTP zCKqkC50a{eiDi%t^qVIFZ?f#pcXx1*hQw1d2JwoVZSo@Evy8M7gz}va)j7yLTPR&353!E(^c$ zzVg(l&8j(Y%-7Ga>5Sgqd5}fA828ty5yu~1Tf?sKj@wP3lttt9g|f0+)=aKhY742p ze_7>NvHx(|@+F=_-g8-Dt|eee4|=l7iFoag!Hx<2Q3~& zMn+zG_N`1sc{bj5TpA-IW9QRWS2YQ+U5Bl`=R^i}9k##I;pg7&<$hFQ!Z`Z`Dne-^ zBNId2iA4H^+kaWQy`d2f7Q0Bns70kqMK9~6X+uH7NEtzYi^3|hmddSeviHO}FmWct z1)VJ)oQ&pDdOm6hRG7wMvD1>pxuMO+*`V(pGFb!8H4}d(nfrcf%J%>I{+ooWlA}HpLP#`5y&vUccJtx$d8d-04Q?f2=vP-&xZ|7#J9Q;81?~oy9R;yz6XmOW1wN z?#^$*Zy+cj5KANy=YM>KA|uEz|BEf&m7bHwGxW|?CuU4wO~{1*k+$#Y2?^O2avZA^ zym!5@u#msme^cgKdY8=EBf2< z*??u2;mGru#BNbg>><^9_rr4;`pV?xYWb}9(T+*e?}#8milU0eD*I!Wun6jxm- z0jnC^?re@)Drcs}Z&b_tONR4Yh{L%DKe+1EE@ejwH%nV0*v{!0B4=7luV_Y#0QxtI zg9Lw!fy`1Y#Wc^^tbT*)G-4(UHkQ#Deir5DT8XlQYG>4SZJ1KZV==>jWS#$})*MKX zlKZgdNqf!e`!;Gd{QdtA!3Q)^w+|xrbfxLekd}9J8ZB*FK05_lrWrmDDr0oMakIOx zCa`6y_$);}is5wCAkG)wfpo%Q(^bs}Sz!mxz0f2@V`|yBIGQm35X-dLldl+I2Y5!> z?90jne6=ZmZi2qawo>kYc0R9z{Y^pDB0M>GA^M?41vR?V!!G8 zcjAOav2uM&dzs?ic8@Qv`S|2bACN6_&*iL){gmfFSz{-oxbtknOW{~H&HUTa`w3sh zumgMz>Yx%#%3~a(Sji3C;76*2;uT=-ZKSU*sR5Awz08g;W9!YaeU&7iUK~#4Q>5k% zC`ic)+-&!uGPD*erji4E+d+sE%n55P@8g%9vZapeUz&)&^~$&HU^Gfsn7D*84Qnja zJ4UCji8v7Kyr<*Wd}ig*Hk&&Z#2raYWlCjZ*!ug_gC`5qNeV=%7k_ymKeG~YLzgb4 z?wm!RjwA!RE=R5kl?lZCSp|lzcGoc;fLH}t!yAIdv))R_b1QPw2fgH@jv3f7lgd{5 zh3%hd+_|B)6ApnJqYWYsJJhymB9-l*D3Tu0VLV=XJssmLsX;f_4(G4&x7dq~b5Q04 zJGmFP!>shMwU_-nEgd`VG_LO$KyvyT=~Ra;HnUO~7H7 ztf6S7?5tX<`@Jm+g?$c0q&sLez6KzMBL*w9BRVtILbyZ>6!2jpo03D7AY|Xs{E~m{ z4xr6%X7Wb`psVjIlt!$zjpNJb{Rmp50(YX>JS`KT?ZmU{$$;Y`fnXQhY(e~iPn82t zNL7E@^~NguusAg>&GABoasKOfPhO5^ZXaG$9#^{duLZ^?oPTtj@E#cvligP!Ck1&v zt`x>MD84iom6okZyS_wJiHbroqkzUfW}U5OQRS5IP2zb# zl(z+dzBJ&7K6^F3j-ixfQ&VD-`R%AEw>JGGJ;^C^cufoG>mxSL=ow#Ef*q z-DRT|<-n28vUY=z?jk0~`71$NByLhci!FQpq{t^K&cNZ;jiqK02j3A$?vC_gBY0-A z%l$z^Z8~eXz5M1Ye%Nsfex`1*O15FWywe;AQ8V=j4b~Ja7fYzWrPY4if$j4R*j$D6 zeNn}IB~#z|6B@%&Zr0_W+^K5JnIR&{`ln$AulGo!;r&8^8H@n)S=U_$KM2*#etm|ImDSO_=HL^;uHL zVgS2YB@UFM%O+obb2%@cq12@uCMwWAmWF3~kHl_n0CJAj(vY2VE70G^vukj?ax-ag zx2gn+)9e<>t~mh_cDn8Kx{-ci@3BPCV@^A6q{jB&WX(LI4I2hUCr^Zy9^E()U9X<)F7M^UF<~g4}+*x+uk;9XcuQs$agIX z0>em507xR1dWfjci$giGIgE`MF7unLRS1Rd;M`HW&kyiQPQ?+47rYFW*MFkx=wp20 zqi%(Ya?f$y!Kkg!?Q6cG@)}kFCFs^4s1m7C+bcljs*2^SNCR#d?DTk=7N$rZ*s|94 z3t4Dk$(V9p=XrNIyjRsfEfhn8s=a{m! zCG=F5G3SQ+hZ)PU1Qe9c%Ofen6U?4|QKxN@R9NBKFeK@9tmNL;GM8W2W^ywH4^IZe z0zMil+=@TuvlHi#vs-_)TM!s}b;6lQ8FG)>4mg z&f1S~#zYo&(yejN5)21|KcN zL7n6ZRLAEyKl=FUZBigxm#f+1!cNdhE-6tt15~!U(}Xw8`{v&m;F%Bax> za!N_5{elfuURoIm69nK)e;>n+b;N9Bt+;r1zkb4c@tbthaf!7B9}}1jTKo%k4tG<7 zf&0No{IOd0gH%OPiY7(e_ENKg0%xJ(JMMAy5-63g@`!2yv;d#csO<6UC?R#rcCCXx z07B%ery3UrLUd%4()W2ynjbTb99B3USA;lmXf*n;491XLeXZha2f}{ZYanPIU!%V- zPdB(?n!U`QdXJv1B(0&8JwPzk_IeAbK|?yoTf*yo7Kgxcqc9!k7mJhC!H&<}wg?;c z%t;x}T^qXF^$e3Q=rH5R!qg55a{A7|#etOzZ#}gT)dIjb)IcQ4oQ|ogUrI8MJ$8I= z!ed97zBO`nk`{xJvs)>_+Fdpljp?vgqa&w$k>bp?5H7PnQ4d=@iYM-R+=fH+Y^&Qn z0xEp;*5e6EDu%D5_YRdaBZU>%xIE(BjKL7R9wXSi=RJoorp9u+vM`bqd}{7@Dm7%%T|E4&sB+#mMV*|6RW z85VL}O}p@bl9DBQSlXg1Ts9pOZ3%yET>0@q7M4t%?~3Vay5e5eER}Hpwac^mMDgXk z2>#Y??x1-js;OLoYk{AoD;@tPv~$(wp>Q7bwFV*-;%bFx+nbA+|H%ACq1&*hb#qVfdZRv;9IXIx%LfC=xGlp2S@7hlZwFJfFXn~ zaCFF+Axapc-o$v*OQ$&bxAxZU1$y0poKYdTqYltFHkB^Nvt``qjxIvS=F$Gt%|+45 zdqh(%U}Z_a@_}!8Ij@Y-qx4mYphcW=7I@}zK^kZ@M8iRcYqgW^g?_8~XMLjtHL0~p~LbrFjCqY zzQdbu<f9Px z|H-P*6a7$;1t!HEQqS1_2?YKufY_NHSnF`DuYj6|Co(wkeC=IUCQdGJ4}EmhBMnQ} zvRAGqDtMD~Q^Z66`DHgbW$ece_dTaDHrkDqe2L#N7Tq z&)L~O?Sk`r9blq}nme-oV2*121R5OyR}=%27T@>PYa?ufv5#T*>df^yqeM+2fRE%h zZ&Dh0XA983$QA+EUvGa!P$vg_P(&VFUQPJQgLcn6>f5=mkRLCfq^erapfP?J(}J^Gl+C4ur!!!bySW;a&t=-1JI#>t?DxuZ zeQ?5Udu&fVy2*=79VA#Tfq^Rm?vEU=dnslFA zPsBWEp06l2t6P zHUCCms_1?=6aMRaHkQw%#Q)Rg(oA}OxJBQ$q_1?8_>_7|<4G#h)yHbgL!7d6yh%fj z9GDWOQt*!TW%5g%a|$sMF*T7=_8qMYI|u0d9t8Z^#aXn3scc8+(S^TH-##c-cI$+o z2`Aa^4ib>eNgHCa=HF(%%iop4vyE;}6OmV?lq^JL@=?1IZTETn{cc@hZ-M%LUbb1a zxKWx{m&)ewrsaZmrNM0kc^E#|#V?eXk=|Ek7ZtR+u;m|Wt|@FcR`&9SMCM+K^{CT*}YmU#dlNVyMn4)9yN z(C?KAQa~^@20*DDb)?s}0tHIgCxL%q??N=EJk^+!1i;~^3JI`7!;PacNf5<6*6&CiHh|F-CV~& zCDSbTJ;qYDb;_3L%pBzDlnBflxti@F@`r0UFLYZSdMQ znByq*8z@#aI9=e{T?wT@)T7JNaWhhRV)Se;6EW1kdNd5HelKd9AJV@j16lck@QICJ zlwRka9U=L;KE@fhA7MWjg|8j>EcAXs*5Fsd;+=fK1)dJa6l{zu0oycnwjFkn8QHSm z#ULo|x-CyUy?14fQMU0NsP$(iFSd{MU~osnx*(MK+69bs9g!ZC$bqf3p3M3tCmT#z z14`(lE6}X9+s%FvLIK>gr8Z9FHK}`T*M1md!E19nXP?6yuVLh%H-wEVt}HHLGJA@CL(YJEoRaIl4jXlJDRk)hPE}ZV|c=lCczvaYRRMkyHNU zfLS|)Z#GUFu^N{GkZD`?mg`)x_xMRH`xI4nc%IA^YG=zJo=A~=k%Qf3dYS>I*;9UJkEAaDw&#I zH#aLj*Fq7+WU6pElJv>dccFy$K3SA{w@wsk7n3`1e`*O|IU1417;|L5-+Hy2Jb-|C z4WtWhv-Ul}z?_Xo@>Rv|IxK74C>;{X*=Jh59O-g>>dWyPOTOQgoKe!zd;#WKe+h}A zztFb;p0FALtP1isZ=u3f^A0>61;_|IuX+pHnC=9}7@wGh=psDXCS>r(P$zbX?$xt| zsvHiiP>*#n3I@HrG@nMIN~zzr+c(zzQ>9xVg7wFXKv&nuRc(=LH;ZSF-M&7DX(+sK zt!^Ym0ZR5pHjm%cANH@NZ)k9YtZYBzQgG6P z#1}Rbb?fMW7kn+GF8zhOKCNZLq|+_`MwZ%?^z(tr_{gU4nEnN(_%Gumb=vW0F4N`I zFm-cVYn0OBFYPI4r(-;OvG6=3IgMtyA9;it?`r-=>XI6rh|kg(&Cd-W1D49$dHLKK zBm#$u41kcRGxaZ?(Ngk{sT{Q_;oBSWv&k{@k&gHo?T_Tf0rV0}#r zb%)*h*yW+2D!4@4kGUkQJ5TW_Eo2baa*e|2)wmYss(KRNA$t9yuZkMB(7bZiqN!Lf zd)}77**}hYadzWm#n?aQ6pviDLrV&@=I|C#dL3$ZTrj415JlVh}=^?Pc`QXABtf-x1!1u zEpUm{#ML$rRW(!h&9fxaS*W-cCVO)AMcL<9^o~fP5`N`b#mQSQ>&;olaxMaw4+dA} zGpASM@OsQy?t&%Uu09Ly$`!gNLNq|vCP~GtdRjemmrphZm3ED?lnOoU5L5rk@<85G zhy&{+t(%db-sAqc?pXM~s!GbD^skB^T-uT}*GVRqU)?hd`}tb^CRFMp4C|q5tPnTo zXYnTK-l~~^9)A4u7RLu3u@xgOD@P#vE_&JTG?Mo^-h&MOb1C4gKi!`Pr911L2^-jW zH6DE3h8FeoJH#c}UfQ89hABk8ykhDYpFyLy^Ash{?j}C)ft5)< zQ7AS6keIRv)l4lp(9h~&J?l|JuL672AYXbH18eOcT=8ojXis}~-h-#1K4-a|6FUBp z{eL33evMi!DJz}ttA$qgE&fN<>JMW$t@L95|A8TVtMR8L_?nYb@_97!T&hf*%=!Ps z)>Z2;6S=y)jEE{d#{$yjq4nyHXl(?SV!&VDYg`PyP^G<32o}F?pMwF8dhF+DfRC9P z#M`@?g_|3&w=r#P)8Ayes b+B|gy4@l5`^ literal 0 HcmV?d00001 diff --git a/docs/arc42/revocation-service/images/issue_revocable_vc.png b/docs/arc42/revocation-service/images/issue_revocable_vc.png new file mode 100644 index 0000000000000000000000000000000000000000..2174a51ed1c8c29259cad5000d7a4064ad8bedc4 GIT binary patch literal 43766 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ʇ=^? z^@N6#)R2se7WcBK%Z-epqey#-)$*}m<4owsWBGA*cR8p#^yIfSBC0=gWAM-1&>}#f zXb|UazF!nCzWA)-2Iy8`!KRX>5U^=mS>S#HoR^MYuq+vV5c*-PjCJ%Ew0=|Q-p(Cw zILtmpsIF!1e|qS)Zb193g+gUMI&K{ctu~c)XPD7;b!-D5@h3 z*G=j-Z|5ZQ%=fT09r*q7PTN*4J(SzHcC6a9n++TuES&FItnG(jC&oi1oQd_3@dZ3n zHlj$Pq1|L5*;gjZ3kgr-0kwez!o&k6sOVGFcFL79Q~CNlEK}jOaK(KYP9u`{1+KpE z7+x~XS|I+Y+$QGI@P?V`#>UNm+?6K-V#<5H=8or4<<;LlQvq|1c8-D^8EYo_llyY3 znJx$D$bGWLmAHICWr6|<@Q;i^O8;t|KJvcsoI5DnO8R74=Vu5=Uf(StMb_Ah@1#v=qV-(4&%HVeJEyI_S1!hlxqi|&u@OEeF4Ub*pQ z3F)DX5AH}+ypacE!I}h%n z(BDca!5jN38EgyWc0rv3KVg?V$E#K(Pod7i_2%-%u4I-Ax;V0bTf-CCMi=`SVq_fX zToHqPQq>Gg3{k~DY+6-svytm{bDDR&yef7{ZDUiTckO!fy%(o9UX^w|PT5X~BhNY; z`&P)-M8szJ0txLi0h5P!9Ktme+P}&b%BOu~dDhf^GF4!BQY^vwYWKN^R#@K)vFEg% z`$e(XOG`Ffsy}c?HX-Dy_|e|YAN1k-t|bkUN&H1J!$Wo_$P~-EK+d|w)=Z@>6Yz|U zy75&2m~muu+J%JEWnAvD!;N!}CXspV%W34LpxZWhs-|b{?uYO)V_8rnuWbdvKZC%1 zgJYart0Q7;Xi)tNKL{y>2>%6Q_EhOR)VsmtRG5S8gy^0$z=M&e6YtbiVgbVcjksfe z8fGY{umY<7hZ$;MV3qg zpA1qNxZ@)ERa9bS|BFi)1*LZAfEpbS+zKyy!Zt2^!Ncqn?nG9hQ}2L1?)SsyYx zhpfx9qIK5`EN-zo5d0?#l9i!ei+zV6v(|^5c;7lJ9ej?mK?OEryzmG>Vjh!YjrM^f z6ncD@K+W})FDGeoK!@8nB@%4Ywe?irIcI-^u--m-b;K3ip=Iv>Mc;g^=Fb? zB5RhxnBOA#8+?%^8-ph|Lf^4d`uVvaF3d%>gTH=!OniyLY>0q0 zP>$g}x@tzTJGtN|E1#YDtM9sFyU&CS5avu42TLzJZQZ?*tO1@t{K8BCAyB30iMEyR zPc-wvv5IREw7O$(+{9JL)@Xo|y1iQ*rJ zA-G-` z<{>BTF9@sv@jQU59bQpmj!?nrldk!3_{M`)%bn)>@cVCA{F9Z{)i*}CD`kvFSk5eI zIGt$(aw7ca8~n`grgqdPK7a92BlX!u5$helzK)KcqB+$sJ@gm%`wHfkGNR;$P8YXO z9T?+Vwhd+pfSrTfI1X=bm5*!}Z)_Q;&!ayE@UKU|Ms4_NmK8jg!8Xf&=<3o=E2FJ| zN1u^(73`2h6}3H^^jm?Dd+YZi+;GO@z{;=BzT(>ts}wIQ|K!-!>_mr-d#SdY@Mv#0 zF=Z!jk=|?G0^;-tFgF)~S+zDO4Qho&Db|a7@-lT=Hu(`VDaUd zLr^t;tO`^zLl#Ib8$Z`N?nzL7>*cR_0hM5XcE9a}p?%f#(*qYna?_3MbLtN{nsDVd z(^3#d*M8w?(-wqC?-a{V7n8tc)YP~F97MlN)|6!d<7CKRwfD0IC41Ed{SZGP#{)xx zA>tR$OEC6f4M!!f4av>44`!(&i`xzKzUUbz zQ}Wanluv%&CS)WwTDa`BF=Q)vn3(iU*S*odm|Te>p-Ux6VQx>DZ_MyK(i2boYwN@g z`?`G~zuUpamO#tXD-0Y@JI~}TyDX->9W#%T_1&k`1o(4q%o-z;xuYZMYKy^rI>N{n zK+;TE6oDpgi2@}*YEHi>64-w{3?|uKYZ@$)Iby0bTpG=$_~N?@r9Q`_bVY4qnaQ0r z3}};Bl4*6YIG3p-N0R6>NU%i5EaLPH_zoVJ4}!Qk%s%K7U-A@?73`ll4B3@_+-v)w zuzpQE_?TrRWu<}0v4)Tl<}9gGSUW>nX38#c>wXICdi~+P;~_&_mz%DGsYTPeI6qR9 zp8T*R+N`k)I`|O*68g;(b}m2D`+s`(m_*q(IJ4&`3u((b1u0JWJ$=TOoB(-7hZb(U zOc%@kib+-2-2~f=S_KYw_p`cSkya~C!Zy|cweY&8-vI&R6NXaNu_Oq7c!W&= za(;ix^z|VI)2aG_4-@_NUc+YawAdJR)5a0e&T)Csw;5~#Z+dm^B~l(9l^7Y{6=Y!L zTBtYZmb&@)R{#ew-vV`c1@Q8RzriwmAnNyAnrq{NnNf@j4ZF*dllD>hZqG7Hp?WqTG!EZBP?KyTyK8PVY)}^x%KFQ9F zs8POC^o!M2UF~P==6F9$7(sEeRV~mp$BicGh5G9kE+N5gv?=l0_VntxH-`)gWPji{P-~u>&`r;b&AZuB z5s@%^y|=z`x$;NSjShTdH%{t`Y6Qd1i+~YzbJRwvn zHt``(D(fE%qm53LDltkJC??0V;1By}K_P#darmWGTh*>5scQC63W?v2Sk8J|*MsO- z$5*EiyAJM=uH<@Ixv+H}nT4XDWm~hS9P54s-*j(oaRvRP=?J1o*X(hZzO;ysL6?qYaGOq%`2F&)vwNyt67DL<_pUMR)cf@(=cxoI$?Ch`>{ixo zg?HcD;VSdLkeiH{HXdD*&VHp;D8L@}h{Yyj-|%*Vg-}C+5QP=JU>WN3vabM|C zToffId}=k7l&cxY*JT6l&A$~N(NOZ~F`kzb|c-c=dtUYx%$u@tLayNV+;k?VAz2U*ZZN-#dQ?s44%#MKGH&Trx zRaFCMoVw7KfA^?!S@dmor$7xD);z2^YOlyXnH(b(=7}l3o!Yz+_x7IJb_%^t`#^Jt zdT8o~-8{?W!8IO4rw)H|4Q6Z*RUm2fQM1>W+xB*iFU0o9`@6iU+XiJV>GnkTzb+Y% z583DQx*bF(IWB}CxZdr=b)zjW)zvbw2Dydq;*PqU!|5SEBfpr_oKw2)5sT^WSj3Lt zWW)o+K9v82ds_5x)Pv@8(QYQh%UII@29|eN$*?0Ar9`eBySnsvk|V37iAMUEf9bpH znGRLQXv(F;*x4uXC5iZ7*}K_y1q2VwT{B3Zq_G#Kie}B;lRVEH7J;2SMu4xY>O&jd z$b6hVgm(&`T3zeyi}KC_4uPVnt@6pI3aSPzNtZ;iIE{xN>64xOI`7KCfeP=&bo*J9 zI->zEE;L>7luc zx@lLOc;GlEwG+g0+equALJ8}uMN^F9K%H}c>Y6M^IM3~FTHjp%a(g$Qk((dzkxbu< zzN4PQri>9{d;_?7vlQ0rch&7M;X}pboM)^zV{b@~7E~|I&8N!YG`WhhyKbs>NVw~v zGvrmbQpl=LK}@+_n5gd94B8p1Od=e~ZZdZ&C=exAYo|&}%v-MD+ZTfgx$+B0vA9o$ zooPj3HsK3$-xUN(B43n7BNQ*ff|^fX{!~GieS$tBy5xi`!>DTCaYe+ioIkU4ku#s8 znw)`t{ma#5n6X=$i>g?KkTV+AGR8kdpS|Q>SG)9eNo*-!4RJkfvDrwjE38futlOC! zfRCe4BGy(ufdo@1g)n|D0hjzvBjFFHoOWAZGrXdCGyED&F#Ff=^&Bu{8W`$#)a{wO zmA!s3ov^XHB56r}7n3sj&Yn5m#QS#Qve97sJp&EapOs$?t{?ZnFJgSNusz!#7Mmr1 zJ)+TzB`>Z)-)!2x)Dd?%E3cZ-Og5OmU52!R|LHgT7_KrG-+ra})lHg9o;QSRGy~$+ zR%6M+s#k=2$%}?kttp+XBV>Q|jRy#=dJwU=M%U{qhFIBnVJM_;IGpE*9tyfOt1gw( zvj6d1bilA9?)tgP)8Rhi%CkKa%APt-w(Y=!ok&IK0}(LoC+B!WRyee;~w{VA8QBb()kh+6GW z{`G7XAG{GyZb#gQ+A4l-L`f!km5*Q3XNc^_^SaaF!@9nLQk#0Z@vqMQ zVD)UP)fYO^iF_Zev1)M~QbsaIv(;3i;uY^Y47tq7t;WhaFUl39dRD)xW4DaodV7lk zDLa`$=BVXBYyOLO(GWrGhfM;TgB&HrH66>;xj5r4tUP^F&!N+;-d_xk?8er5 zothygG5uI~_w?12l`94mCKuG^j@qA7jMR>lRpHrGl~oCmx{hUVvkW-Bo-N3l-;_U8 zBEg-_didPO)e&|P;)D$aMiapg zdbC{LSW&K?k_yrX!BCr`S1eulXv>kFEJeO^wXcv4NpG>~*qq(gi zyaF;@JlN|?S8g)PuV=T!B5i+1!}pFa3rySr3iKuwwah23c8OpVWYQ0V*FFzKE1XZe zj1B}^#M03u>=A*&oPFpy*+sP2d7z4u+ZqoFv*)smGOmsqzWsHlV=yZQQAm2iQryYB zA9sN&2Op`=Da6zTZ{Wy{|aeNPg+{L$eA z;+e^vfeFOzn-813yO?v74)e?KB2E3rdV}Lgm4yBM%+YgkF2S+SKE2SMjGSL%E z2%%-k$!d5d)^22i0W*qsoW)g+-g(R=zMS*Cz8re}ev-+B5`s(CUM zk|$C24#T($hOLz^IXj2gj}Y52hs~ejlDl{O-<~PJYLu5G+*5h?i&gu*Y4o`S@F*f( zpQYSlQ8?3c|Hvgw!`-P@#teKu--rB0G+y>yEvSKQQ&-xepzYk?uz)TW@pNIeWlNwX z8@r|^J5{LjC0^ zkakxjG4i=qygLC#q1s%8o}`C^Y?&LWF*R4V7IJp?{TpMiTdj=$n5B1dUA5X#`iJF& zI6dKKB-im^=w9{RbCOvnlq}#||50!X3uxib6REEW^>=R)8wIm61RF~=dkbN3v)sBC zVI9Qfo%eOwsubuxvtlTQq~shbKVG&l%utm28E^OS)>pMbuKuN|oktbA>v8;!uy^nv zxC;3a@_7UHlZZ{Kfb@}fl5Q9LhK6a+f86#VEEM^62|+MnxIpSO(X13a(~60-1|tRK zXfD$!n%ByVQW%_Q#B#gXY*FS9X->rOBo5D7+j#EEb>Tb-&Em?iPcj)k9nJHy7tdoc zuLdOwraP!*KCehC<>tV`485o0*lV68K8bL=7o&7iXU-h`eVk2xM57jSX7fh@;68m; z8yJ%VskYqQk^Ajk3I$1Kva#O?8oX>O#O-6rTsamgk@V!LsRzR$a(&iLzMof!d$iJ+ z{&c3X{Hl9?o%8h3LVg~oni<;39*rf#1(+n;5!MMTn3qY^UH*3TgbLR+10}^6BmX(n1b(Zj%z#uAppmnkpBz?I5F%h zM6)TPK}sRg@NozadMQW{j$n!x57ZE1_*+Z`s{nuJa!?`gah0Pn|KnvO6v?@%l+FhI zKVR^#i_QkxTzN>}>q>?xYQU0J4q7F?{^$WE`?AD4`FVRYPc)?4?IEpZe{ih+@SwUH zY|8g$K*~3I<=kq^C&8f$MqZNrTFOVKRhjRq96CaYcB8%uPJ0U-bAscW>g5MsN@qk$ z=lg(BA^#1Jx!uMqHisKMqx&+>`}_r!FRHgD!}e!@r$Rfnx%6!~*=MQhoxAlaB+=;B z8B83v{H%*WY+O;?4t%N^t5iRCkCD5r1od;0X1jKRXtvC8KKaT>%6kTD=1s@|5!KJq z!#gf$FBP$fT{hO3-&Hz$`2!ga{0^Kz7-5^bEoi(rFH#P`qbGFs1_QZ>0tV7lQ7YYvH zz%+L_HnwyZ{xJggebZMy7av*t4ov;c)aLo4^~*<;Fb&00-4LNN?&nXrT4E?Fa=K`d zm#<1feSjahZ*6;Fk>gCxx*x~$tZ9M<$N)@q$shW93|hul0oW|xs?%n-2p0XB`Q+^< zRi=-ptG+)rLDc28zb}EK-rAw2Nc)K;NLQb}2Pwse)!+%$bwz&vw*!;_JfK)$>r_^M zG43QSLTH4#jc#>YVi6)0qa%@3JxXN9hxrR@N~%Hk^Sldz;*_)lWb0hu%(8U(l(=~) zYq^D;E>EcMtNQW$gDh-8pGNVFZfC`fKo(-!UIMas)mOv^TTpS*5>%{y_m?#iGF1Zn z8#$y+7yY6xS_QS|zSD4K+7cLM9X#BhfHasZ^h22ZCX@%4fxW?XDBHM!!#lRJ-Xw<> z^mKQpZ2GS|%J#$IZ~m_#xjrSop!2m$-1)lwo3A5KMOZAHuiYG7!;Yr~9s4zy3PP;@ z!&-GjdnrPEsO^gA$FmMF7+kGWys{5dY&jL^A+J~_11Zn2n0vmpW`yIAP<96uOx1Nr zo`3UQ`#3Yd2$`%^e;;Tszh_&Eknx@CgXa@}M#e>hFq8S1d2s1FbipZ9SC7|MgY3%h z2ZCnSZ_<^`Lnf*RJucXO=%ixU+UGM{7ZSIjE%wJy@6n1TQvOP)5q<98g+FzY;A-#{ zsg7ZHk_>ixn05KeXW+ASd$uZI%~4nG}h*tyr0lIQ%$`^#r6 zj!=CjE#?r0>E5b4?TbT__N0Hw{Y0xC8;-5|N$WLbe0S)IvT3qJO) zE$$>YLY#sgeRXII3;Hq5=m0dab~sfjISXpQx2T8ZZ43_23oPdL!~@JeoUZttI+I|2 z%mestTiudrk9eNv>McZ1uqRBpR*HA1T>Ax|U$d;54CUWjM)TJaeTL@8x%d{)^8k0c z)A#ST?L~Sc*h$E%FUeD@5!=D=eNu0*(4v+dX1!TM>&!^*}44U~4t7ocSC$yb~(G@g05!wA%exip2d+VR20J40St(Z#X5W>@rJIL4@pLX6dCsX$O(ge| z%}PItyP$=_U0_1hexzVGW>S2<+`Qohr}OP@Y=D4pYl_p9BCcrIq1uZB%WR$*qx!2A zpBgC>-hkC>nJDEPdxifq#8jqC&{VfY&e8{{!y|C5@=?DI!msA(+IWR2nR4@B`u@rhcd%j!}~tqY7_)RmPXl`z+9f<(nfJ$jfsSxHIIEzaT5mp+w z^YZlw9{IU(l#wg1gtH<*GW>(mf?PNaU6P#6o~J+$0SvRp!#GVoI!QYm-gEcqK+O74 z_i*5)cFV{FeUfx<@OrS(`su!9Qyw`eyhe>Uw3=KfEw=0C4tg(E%43Q~Q|zw>uRms$GQ+y&4U@z6uGBzmzZZO28?SA3lE&6LC~UC{rhD1 zp}egtY7YMRxv$C7>r0+yFkqtBi0;-(P$6Yf*;4}Ms$Mo87UBuzMYpAge7P{uf2Sxk zgNcNC1yi`RT7Usbuy}umuMu-)ll|uiUZ+4>A{&M8W* zbR$f|>;P@3@S)}ot0d7{1kTA!*l>`UzmaxW7;E0EHpkdXGyF;AzSL*P%)X(Yc(5^} zL5$xEYsQbm_p|qLSOLx?w>~33Q_g6i`Ysk){mmI~sgpnT7cMyQmy(?=DV1{28I`p7 zah=0GceT8+!~4gql=!q+;-^!s>7#1Qx0uo9%>aOMOvOKr%+3$#FznG2k%4Yj7pwyJL45`7Qy}&9p za(OaolBM5VElgi{2BN}gl5W;l1Xx4kG2-a@_Jx|Qrm@{ zV#|kQ2r&xRl0KbsX$>JR5H4q8KjP%f92NJWlTz8nzes;CiRpq>dBd|9Gxo0?ju!~&C2Lg*@hEtuj3A4{6NGnyc8%}P-QX$d5U{pRF zCKe7R=9IL%5F$6kJ=g>vScrISG~fYZneBSBsmi3jO_z<%xXZoqPmd$F+5!wTFmiV- z0xGEv;Hm6JcBaxa(cAVxQ4O%b9}nLOJH2OJos!VIsT>Mtdwq(BW5ZJ&Y&_KB>gNtv zxII#O4%}}nreBhEmoS02R_Eqd%J)Ez$oA9hj4tuNFw~v)LblFV?aMDLssnQRAW$0a z81e@P)si4DDas4s&%9C;ACPy8Fe;!pWNsepRWksK`d_DfEMPoYcvCUEd7(K`1Hr$z#k zBPe~_w)E!lre6jm%z4X1B*(J}QKYg7m$JsQRm1=KUaJ_t@i3P zh@4y@2-DkhdkAHu*tm+&{SO=Syq1gBT1hz!&)27K1U#ifU;}+91H6oH0PKF0gE7$L zt|3BifgwCXKQ13nf&(5?7g63nanylKaI?DT$M4z6?$;{dSe_lqI}EN`8_7`Z8-X}# zktflvWYQ7H_?2+f&d(7bPAr#?-SgQ5toIgWyI7yhPIA8F@lmXQ&oAwe$bSruzuO;^ z>;fpdd~fIH$%>&nFRS3hy`lIS04*-4VVpc$ed68s4BtT6y0{tTe?g%Rn37>r!|rqE zge@6xpi&z?UDC$s0QQ9Ly6VFBW#B-w%Zqd+WA}Ee0OI6+^nhE*lR|@5`CloCsyTf3Hlw7(CJd=W{Hg5L%)UpSlt(+6IP+ z9iZ|p0i$8d+Za@B-+)v^vnU%4j?Fz4$L&f6ZiRX6P8F##-afGjt#Q157~tGq6@@Jt zXdlYD6}9suZf%gkU5_m5PDkOn82N-t!N~sRJ^y;f;HICO1RS2X^|t&V_Ks%K!wob% z-ga-WwtbR!qyezy;s+)F%+m)j@DFxHWZt(Hd#eaJv%CjM@O0@tsp6sck+nlZlrho( zGk1S|DMXp)Hp?}BvqWWUx3izUU-pL>pLSQ6IK41jtj}->dWpC0VaeJk931Op1<8N2O0*9hyFYBU`)F|+5X;U+)9OkDYR7!| z4KVP6-%+;*_C=KUECETc^}7I9>-fy2^{7}sm%18$fMrOt`3f*@`u1T3xJPrl3nHOe zj1U#4^)90JNzO z>KRgShWFkWagSwwj0j4QsB@v}baEm;w}SJqIeXR|AldIoDhkE3*FyZ`od@#PT7YmN zO_+?SEC*WH?xFJTyBeUkm@sSI#0{9bI&LIGQe`iso)kUYvCyBrNJlVCuURe%D2 zSd>74;nfvwd39^8!m=PV33PaDCTc2b8@kXhEyu11R1S0+WEX-A#hl8~GhY6{U(}Hoge2{!iXf6;>7I-D9*23ica#|3QtJCi8?4dfCJ`c zD`pQLkM4k0bfso0GAIJI%lWE&QrtjF*&%vNr@s}h zWp%YZC)xcu9bL%XE}8ZdPiPiH7G7+386T<*(&_x|bhnx=>3N#raCiDE3Ia%O5o9HL zC1_QNpvIj9y5G3|MsY!Nbl@gB z%x{eO`=?H%7oegXL8~Dyr0MM~nxHlAG>V3!**wIqtKNjBW#;T1Pob)K_^dow6oWO8 zd@Tq{LdH`51BaamtbSp}=MnNgrAO)d*d?e;{18*I#^BYrM$vR-UU#N1(Hicdh{!$$Y8Y*=;X&j0wqG!7{Vjlt)xTp}xvsf!DF4U2_v^LC*Te zk#t#xqE81({%0XxuLc58KN$`@MN79>>%V{{^x-yKdx_~*xd}2iBGI1D1q$QU@&@poj5;d7;%~{2t)fiS+ z$)_*gA$5*;;M}uPS^m0ws6(bzvgkUJ4D)4A_#unC;i2DNb!_v4l0P%ePCe(u@dxHb zk)PC%w64<$x0tXQil^dw@9C>ueKEjb#!j5ak56)2PDB=R&t~J2pvv4YYlEED%y9fy zj>9(MXa?fORc6oe06C^cdDVLH<4tWuOjF;tBCcE&3j8j;CA%JS`gExd1kd%Faq=Vb z{jADGbTh(sX9>_5MPbSDn@&U?co4td?UOmXtACy1c*r~%b$<6<7te3q?+YQhBcPM3 zi6Xw>#R0uxFwk$;>3ZbLb=+d{1%|w}2Uzy24p_Ybe>*0B6)B}$I;?%h)SwL`y=nAo z;WpQ&>lvQ)P@bH=qlzTR{#@QtkRd;O&*EXc{DcBA^Aq+oBSmIgV=0~Yxh!wCZT~8` z^nn!lESY(%x#}{ODk7ec#Tx0zfJtT z%umJwr*Q_02e5p-^gQ+_(fBo8sP}a6c9_zzN;?sEQ|s~|O0&3uCo%S17zOX=UNS%C zoO;twGcfcBoAXv%dMME=ip)efHB5+Bgixf1hu=Ym{rpL_Whk#7RP4Q`jD2#O$Btxy zuHe3DaBE25nBk=aO1j8))sL~lC;znU*s{Fk)CPT%W79f)=50!!Xmmr1iOn+qnQDQk z{fs9;<^&gR_HwxFOUY!LSz$jzz{OPL}; zgH$=^U}@wynQwoQ=#&?CIhSqJG|wsYw@D|RC&*qIhbVVg^*q(ZHY~K?n^;wXWykn1 zr0_D=EAmKoQ9wT(~3niu@yHr5n?e5-!eEmuJi4JACFqts1lj0JPp1DuuR*C{Z4dP>xr)YnUC%Md! z0L8|g2)kQMPtJiU-tQPsF`8>#+9x+Vc%q=tW8FATby?uk587_}4s$MoB<{icLC2)q z93_D1c|R+~=v^d5@qJ5oI;1tDH2w_>^D+A;`+$Z&!q0C2L>l*veVqSBzi#0ZEd4=H>Kj(bFm@0*E z^ozpcKJ0{97^e#sCnA#7UR9L!?JlGi74rYRQxvYAVCF!*9qw72QhBL_oBU!9nibL6-d=al)4RA}xbKjoR< zEU{(J{jL#^;1n+i9GpO;DSw2j6~}mUi?w?r@J<6#r_9$E2_EY52R%0^(d6gA2yLOT z_ixFMV&*;6^b-G61OM9+-zUFML&vnQo+|VExALC~U0M_FN{GgJr~zs-5b$xlB?mMc z@>CJ45(2mT|F1Ck-{S56@uMM25hk44q4>Djq1-7_juv*s|BtUx5i6tDgktCM|HRpU z>YO2w>3mI3|K|$k>H9p_?^7iIFD1)=&k_8Wd#16&~aCeiu&rS{fZ_gY=clz2a)&E-mReOk!>$^+* zi6QqS2?wD(%b&&%wz={@XGK&#ydcPYvH$N^Rl${zy8ZY4+jfXqyndl*sopsVfuD=# LHB<_eOauQ9W1n=w literal 0 HcmV?d00001 diff --git a/docs/arc42/revocation-service/images/revoke_vc.png b/docs/arc42/revocation-service/images/revoke_vc.png new file mode 100644 index 0000000000000000000000000000000000000000..5548249c0a2465de88e9d62d0803f728e0000e5c GIT binary patch literal 6054 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 literal 0 HcmV?d00001 diff --git a/docs/arc42/revocation-service/images/technical_context.png b/docs/arc42/revocation-service/images/technical_context.png new file mode 100644 index 0000000000000000000000000000000000000000..e307375b300fe8cef2f6ba5d0fddc62c2944ca72 GIT binary patch literal 280380 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 z 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%0AuH?C}k8>5~76OcuF@$n2FS@K~mu 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

y*^_0H7!UT77qLrxP9HCneiNc9HQ z=tZ!mg?-?e;=mrE0VzZ6S)uk#E`thj-81n5EFhyk#xNf7C3Zz=+~ag2m7uwCMF~(> zkN)d7j~~%70)k~?;^m0rb{BF$L?=+)Eh|7RE`GRfWImxtf&IU76BNSKk&Nt60d8L+ z%_^YZ2?*X9Ou|J^{T`@dgM!vCnAGU!9OQ5`~74o@VWsL}X85U2=*KR6xN`#;{ie_k>|*m1Q~X>#NP;^yD@T|NeEOj0P7`QTE;=u9YO&WRsBU z$|x(d&{fJxgt&IHSEQ`0L}Zqcl##5AQbw|W=Z&jAeLj!x@2@<#-Fx4!*V*TJp2uCC z(XtkwG|mcJYxqR2^c0uvRG%hXhg>2n$#?T?R?hZae~LF5w5(z?MxS`ds%}c?pI01m z4IXNwFE)g7>;G^Pp~`ORlN8j`5nc%Eo508hh^}nC0S?Vrbu%A*LydOp(~+CBN0~Zc zH|GP$uGj3|x2e8c%E1_tfPCI)StwWyEk`0myn!a%7COF{p)9|v01$L|KrqE1vknZd zJ^4duE}wW)&pAEBvpRL0J_ReoK19I=y5btea#(QDUg3Fd1`2x7F!^b?tNm-?$K0C5 zG!_zS=IwpQd1{ORCE2ukR!-b)Jcazl@X}dXAe#wTd*MBrHZHmnh)$Z7(HnRwQN()t@2ob zTDm%-xd;W)DfSB37s8u=S{CX^sW1h5#Tyx^FM*o8>be7;SV4$=p-dQqIQ1r=iw`5W z3O#X{>J!7$KPO`nYe+8)x^e(dx$NDmzoYb3RnYLB7yJM5l{xuQeY8?GotYN_zGz^w zjqV@a;0@8#ByrnkXbf8r;xDL2BM%8wfUssn9p@ujC}}#?_yb}Daa_L)+$B^?CEr!wNGT(xt(OvR z3fBWM{IA*pl@iRc3&h~E4&#vGLI>2ek&PFmVIk>SNrhxI+^oE+ICIoyLhE0g-ikC9 z(Z^j7-d2dTN<6JJ0G46O>^UmWVLWw*-cd<`8@SJizu`*+G48QDrV@vL422@lhIJ8V z(*V2e&yse6AXkhbGVtXa51%n6v=i5jmJp@=VS1Y;$CeJ&`2WnvktXkz=3U*Oa4(2f zSC{N&U`P_i;TB#Y`P>MsR22q$Z=&zO2OOiK)*YOg_aF%2)5>#yb-*$T8m&>tew{3L z8tcK$Ss_2VKB!x%BV{((A)soFfk;wuR>$^p69O$qz!ewspb8lc$-8Bo3Y6 zv5rLI+D-cG@bgqBBq{2*qkxxKjOA3YD75JwVgMe{3{PVE*(hTQVUEu~Ul$Dx8k*%WTw%raqdi1Bts}1(uGIbbCf>y2!SO8%@<^3=438^ z5akIv10CI-Re+EqslkJYSOm-hWrLBz-lUV{?ay89=BJST3lNV7(%kbHgeLowq|@}G zVKEN?Y_HHf4-K2U3dP;Qum|8JF!l*2#aK{rjGMO8OE`>Yq(y@_m;Ly~s-1U3BQ+Pl!mhyFgpE@;FiKqy=uuPO(n;uV5iB}O9|=< zITgkBsmk63*K2##cW1~k5^V<`ykU(=c{fDAZ|4w%wQXNn_bS(!!fYrK z1g;c7Fd7BVZtUQ~d#4VD?xSF(LT5AU$2*VV%FITPxjaXe>%f*HPw~r+(qG~psQ8=V zFs}D$nn!q^T(tnmO)3BhDAf@vo>aU3iV+}3zxT@|HxOY~q2bn5N^MNydk)UVsfO~D z!d){b`&t@<;sl#GPT{W)B!SvU1Sn+vRsqy|3idxwC*i|@vpWW2?UKREJ;nLR#-F!A z2{`> z=O#!V;$2+PVi5DZLpI=^zWjQRcI!;+B_Ps85}*&Bi-&%}Nj5|Crx~bmvx`Mdm3TSC zTam_tq=%{f3t}V!7a~p_WB&;B={)a4Yxg9=^nSE=p=(h!iBSC4rkq&d(v7k+TNM;c1xMhJI42Yo+u3hY?-UWOU3Zs5MT?2fsq zpP|imet}4=10b9tz$r7|235)?*jqqR(7L`C0oFr*UIYf5%g8#=PHFOoKage-^aK?4 zAUi_xXZU|yfr=VSu2h$>kQW1-DZpk#Q=CWR*3@Rh8ZZAM?*7~hO=A-ce9tt3@Rg2Y z)h6VZjFyn3HQ_TDDG36}7$OV0`)3bt{UAP762;!BTl6-OFr@jH?&5lwVP)2?_qQap zxQa%6Z3FJzFxdec%xz$o1Rsm*tNXh#nh>&yW;P<_-XJ=m>J<~_x3FkXB&TBt#hw2V z3Ng*#nW!W$9tI6k{N{=FeQ)8+DtOBk1q(V5UcqA`SZd;(0LQBVQif(}=k-GxO%-8-PV!f<{`1%g2Sb8{T`7 zVneSLDM>PtVoA=XZxwO!DeK7{QrRy8Lrv{b@gUkQO=Lw{A#L^P8!n!305y!CXe_6L z9$ZBIVZS$itY<)h4PUc-Dv%E2%CR!K{zy0#d?WS<+wwsfkEgI8MNd((4_Z3eHymLZ z?vu67{>BKNQL;k}=N>BDhpuzcTW>qyUn~HQ#8b1Ch~)vaYH{!bf+K2g_yMq=C-SPK zB2*^_U)^m6nhr}~n`$lw-D&nACbuO+8v(*r`kbT%chL)IOqN{id<70OH1CQK?HAZT zfb8g?_5t?xC}@-z4NU79oTYI-JCkICl#UvH4b^xIg`QIEj1)YLsCn%fNccR$-1PP|1 zPifKH|E!-UDv-EoGK}^gVDpL9An4tCgtvZ}q@)r8jh55Ck8j0kgS)UNy*l3US`FA@ zxtP(X0Y3Z|VV$O&gRqVBqdt_6!mI`K2M8$7$hkG>2B~I!1(*tFE_nh`qJfEnFM^I! zhhlsCIFC*Q)Yi4TUhB$t!2+$@)*f#Xk7Hpu!WARTqg0l|yF0DoX~%n?7s0-0w+_x4 zlSvw6k=$uaWfbP@ow_}Tjz*ooCv!A&3QTApvdg{o^=(bF`I2v*9FPl~N4Si>@6ke| zwnMh?gm>%VDv+_%gNdVM~V9kMUMiv(fH6mQ&_-s+P60C)cZhC+uu$z z`BW7~jno0_L7%Z=h61e+K~f21yHLqkYK+>_&#z&M;L_F%-*zIqP%?Eh?q6NGo@Ecy zlQ9roF=0PA5@j1aj>ka7IjG?}aO*f#iyglq}A^ zhrE5v9Js}hVP~BBmxYImS7T+`$2h{A+4g(FkYEq{%L?yBgisl(B_BK>)e-}o&sFwz z?)pM7|Jbt^Rsg7GRy<+aHS|joSXz**VFmy_QDCH^A;Cx;ezFdH23A;1<`hB*gz&kB z6qQ*Xwd=Emp;axx83#y44v`ASh3I5g+#&fdMFw|ZE7Iada0i^1z>1T5cpRW7h%2JR zD!$vk;ZuC#Gzc&IEp#}dvOB%^fCx+}QCW^r4~1l78Ab)E3orrG%Z`8?oj~e1I){*~ z@1#KhsD~kK?R`Jk*V)trV0tAYWLD-3?h_13*bHfNolai8a6**RpaOiCjD;Us{`Euc zN#NvCDu8@-C-WCTcesPo=(U0JsR+920y9Fe>V%+o)ctQ(h_vk?wS&zfO;uP3bFc`w z1Q~?5`uz1J&q~9uaKqxzTtAyGJ4DTWryc2_PXe6q;dt_yOYnXpfFlVs>UEILiv(n~K?!>j)iJ_=5*|?wjk4-Ow>7?qso}F+n+JN*fGWzEPSw<9s z?Bsdo=OPY(vyT_K1KLM zm3tApPUXGG?#xFQg()P!EHM8z3%bbbJFkiu5?+sJNjS;&$JbIdej-(L|5;TIg3Tou z6L^UNmAz?romg1s01&^80aW9u4}XaWLhg|2INUH2a$>JCr`l{Ok>popH)QDd%?-fG zopl2G$mhf0y1l{OmKD!M%pl?Kq5(3fUQQ#5Os68X-#u)8Ix9dZQup;#Ne?8s2sD*U zCSH)3R>0IY7m(vT))AaK79CrJ40Euu01q45i-*CzL<>E9hYb78*ncKODb=xPnl}@P zz~>d{EFDBMu4;2?;<4ywu;Ie%Q}O2x1C|rv!+O`;xiv|0K4Z))OyU>ttQ5$r+&$wB zIxL8QNRk)bAqQ**zyi7`_#HmJ(G;0eKAEb_7OHp-Q@Pl6n_j5gM(FOypnk*-OArB; z2ER@y$M3+n$6eIZ*c*2I;T*MoQ_Lk>^b4ClfU3w8U3$~_VcZEaA}p}-xLj7Y&6yJP zcM>T5!pyVmLhrWXIlk0R**yhaIE3WbzwNUm5gLLnJOw53mQ}j}m50T;Ab*jFi!j00 zB(tS!oEH%Fb9Xs#XzN#q`rSdwQoDtn8_dRnZkzsF2^Unibu}%=Jy)~jr)))5oPm?K zVtH$%rPP6JPCDQpYEg0!Vdq1(H6|@0^Yp%9=j4;hp~i7)h>__n6!=hCYY=;3fe(zb zCGx6kLrYQhx3f%g$C@1Sv>;jE-ajik4TC!(TpdJKywUhW2r60#Ki z;Y9Koto-ANxp;5*ex(oUg-}cO%hUjGeksxZ}Y8TO5;7te`KSxF=KN!?!qVSBD^J8@_EOO+Wc!AO?7|SMF$q6+ zmvqS}hG||JBMjxbLaU;$qKpq?Yit3oCkCt{&OI)`WNisS_hn?&M*ixc80+!8MOA9Y zLh14JN7-(=dt(q7k~4=IYu;IeQV76wQ~^5WD-)*oove6&K-6K=3`TfD@-TZt@3pfi zb<#%#GzbK`+^nW^?d&73=dM$*r2?FqbTywiek#OpLG|e(Fpnen!c*V7!RV;N&H#1D zKreSP&YSsLK3iR8!VQ7*ob=*l`T+&dFRN$gQ@&~1DG83P1*_gJQ5b-#Nl-eoP*!^@ zc;6;u6GPnrc2-UX5vIfdioOi;lRVKPkne=R&PbsakAey(g+CENf95?EdsI~)=5Qqj>jyqL^!+hxd+|n?l3Tq z@AQ|T63Z>@C=|a3i$jdlR5137Cw%e{qUHx{z-(h7`Q~C7Q}y|VyPIXwrVpW+M*Y$|&4Z%>M)V52(=Rf$lZ zOIipS0ulTJ$wd1V#R_#!fsMD}2X+9z3fa|PgPg9KG=_z?cnpm9^*%vkRT9S(U`bXJEZ$=N zFipl4Mw~MxUxM{G@!7ltJ1*r2o?X#~FeR7avr9Lq9P1;pa;SoxQi(n;mJBQ} zKvsCUa`6ga&0#>Xo9T2#J<^_gLTXt|xVq$%RZAL>`Pj5sH)h1rIHVrt9Dl+5M?y>i z1JG*v|7a{QhVY?^EwO>TU;h+~4H-aF(Lv$wt!bjqd^&oD`zac{Z=ACtIhsxKd<5gE z>XZH{tkywnUtm992mUe(Uwn-OjMF6d-0ZCd96wsOJduH9WFmU40L?<1E zknsW-BG?ftj?Cyd!LWGg5%nm+7fK-`d!EfeLE}`3Pp? zWq$-^G6Kioy3`u(l-i?G&MbgAtOEObenC6vF8%2{yAPn9$}uaAGEV@Agy3SER8-J& zGEgm649>!nkgBi=mVuG^dN7!#w1240FhzxC2HH1@z%>?Za(?(hF%%-Z@6A2A0;>Ng zI->D2a9|u@y3px)3^4Q))0{8jsEn6>II8;IyoF!P{qd73i=o*Ptk~o)F&7T{f#k|Q z2)Tpke^G-RJ!3ZXCi1Vnxfoex5AIgKha2~Zs%7-JGplxoYeL1LS5kd0*=&Tk`ig>Q`mC$rH}hka*>=WZoUpnD7UL4vRfxXQ4mFZ?PrTxy)ywg; z^E*+NK+BELR2sWl2xS0+hw#fswxI|B$NpP{WmXeh{KeU8cX4JQ8^3)QL z{}Ic+_-BJBMQPi7llCpmE`n4M9D-H{QQ7CC{@wHrs;5tIJ$a$p#BtgzJ47|9XPkZ@ zvf=fLb`l07viOox50h~0d({rbY88%IdT184c^M zT3FUrykhyeTuCS_v6ZxB#+^8kO~}w6$9Dhj=s1r<^t|#1H#hx zOSE7JTqf-Zn9Ew9LiNlDzG9(F;7 za7Wv4--)IbV>9jqcoZvb{c<}=4SUs1p}!Sh9o4t1LAs!s-Ri@ieT|Kh{_?#W1-pQ# zHK(|mBZ`I}2RK}0#Wm4htz0X#)GIQ7H`dA7fWvGk56Bps5bSK0C!oO;tA#jNLjXf* z&~%jT`ivB{O2^fDraKB9KLYY&$;ErGhLu2n0%?d*oqjiwIcoAfP=SpotRR63;Uow@ zZ&Id5p&G7tpQlb>TjMQ>_Nk+s-%Q0{+^0_M|Bq*1s=u9BQF;ixiudoX;wQhdl~0p>bt zZCev=y>rA1Uo^p)vX2;6Ymz=&dDxl@V{1!4AXsbyQ)UYlK5-q0q#iAGFZCt~FXGvL zJUa%&A?P0>;I?E%pdF_? zmQ($p@|*bx1FZ!}=A(cV;=_+Agk}S3wd9j0KI;xo?(ye|dzL&U=o#sLVlYf@8iHOZ zU{5H?Yd)rW(l4Ypp?hps8)I{TXX^zV7a5K`zdhge4DZxvP{GGr7dp@%Bq6=vz8=DQds^1s79 zP7{=ASCg7wF-ze92^{ew<-8(I1EH9%wIkJ!%UxH=0nlv4f(aAb`RO z=a%>8K^r=NqOOk0(t0-pDkT_XsXZ{>UV7R$x57B^{1}Y>(F~77^PXs+-M@Ww^}><# zypf2v1alC=0GV9D%Lsjff4Cu+fw&Cr_hZys#Qq#t%eqq@{E7{B@xM(~j;$)ML{@nNJ>NEsZKK}HtFpqV|53^wmM zDC$?d3~7HfMCcNE4G6i6ufNwoqK<@;M9wq*bD2~STf|Ptaxskna{$_-V!1WFTsfe~ zgfnap=2nAD2kGNPgZ>D#ipL3?QotD|1UUC-F-%S;N!|N847oh)axuP{=xBN=!oMfQd)eBH!q~Y;3YR5uE4PI;%)$aM6EhxMyOI zXJoxl#El#C6>2}P?eieqpIjmxshjsyYlqbG0UFEkBgZ}7o_aw#SzD}dU<0^b9a(bessDLX}u5ZtDhdlgOPiE-oP$RNslvp$nXK%t^(h^>@DlSUu>*ch~`Dly8z0 zqNHx4q~X{{;#h{TBm=4{`ixqOT~V(~j8V~*v*GF5xwp_~cKXsEE%y<9?B|43l95)f z3HySaT#$eT z*qcqKO=wmyT9UenTRG&s?zUnU`ZMgJrr!QoPDfy2%=h$v0Zlqc-R76w4#+wO`x@$0 zbC6n=QQDcU*P4Bb83)XG&!rI^N7n7hUq`d5$4Pc7l^vjV9GfLe7iLPjj>PRW1}2nm zkFRX_%?Io)DB?Tm1&tsx`J@4g}v z)la>L?A@V@@_cV5T0}C}XH}M!9xP;g_W941$hPs2Ur6WbVKfmQ?fDw>b|Ur5Q>Ogx zD53MrCNu>?cOc{w0pL=`$5|-zCdHCd%`G9-Gw8%9P`A?;f=|iI?4ca~!cD7K`TP;f zWDh_^XnNgQ7(%Zk3Ee7@y--hKkeoh^@NI`mIDlm+=p4@GIsFZ0?wO<8U2oiaZ*1cj zIxBnRC%F+6;%vGi#gQ4LIF|Ajd?|biG8gY$^aYZ3HTd)0TjPC~dP|?ttXY4xI_r*K z^1Y4tgOcF~XPCz)WF7W0(!+A#&@-u2XwAi5!IydAu#MpB>YGKK3XNEYR|kH|)+_vC zFzsAtg+2Mk6qou-c^e>0VB{>TqN^W9zhOh-#|zACA2_$6&;k?lZCAZCXtwRV{ktcP z1_R%yGoWU51X{2{{0DZ^Ckm5bQfB6324gn|4 z%!u<_jm!CT@|SdUFYxp~_eDmPqunD4>RVhm^EMP#tR!*F(fgLtE zXUv6}Mr#;d^*vp{wBnJnAOa5Rb44e4#95ZFe34p#ZH-R`BX4w0P-i%z_sc>_?RD~l zw@(lPCIqmf5YsRRL1j^bGVg(>E<-4u&|LX0O{6;MNWPy}5E&~jKP-BsqyXtPmyRxe zL>3wcy(^*@=nOvgRHL?oGQ`@8jni2kWlU-n=LWbzHE?1q(LVy@p6NfuLXzpE@{5ku zK34c*0I~&EVaAaP*5{sWA8UIT@#&+KzTHiRN?c z2X!;{AmeL7^t}zYo3j4haT_BlOUG%C+t_&Q*5xW8m+NSJ;P;FHT<)~b4cD;v(PDR@ zi>!q5{Z<5yQQUS88?3IhHDZEBBs7w9;8vpNWNt6E|FDk!`pq zW1n3-+TH5d8Pp5i^MJ)cm~Q>kVUqSNGNzXH;X+17m{O-eHL%fSAjKq=1*99!vs=^> zJ)rW2>$ENRUZ2(Nukm)!TUShaexIb0Zi+yXRBBVQui^61&XAi!5HnwDc)({9^G1eU zDCyYHQJ!6GZGK<}dSU@%^YHn*#SlU5Bpo{f&5!G``!|yfzJH-TmkS0Kfbp*${2;TU zVsnJWqaU8>R$O;4O0P+944e({XN&5E(99MO#fXELaLEDJ+;FW9%11lr1Za&g-iyTX z8b&!b{wC!b5q&PQzdr4lxCAlS5v8(t{}9z_XcFpJtMorwlx1W5#P+Fo-hZ@X&lWc% zqt+x*S*7^J>AYt_fg*7V?ip$CBY6k6;E{(R<_@E3H00Te;*hXQPMNCectNs%`;k8t z61h=ovh9xr3t+TUaY~7jf`plIeH2|f)m}I~SV0%9Y6?)Pk*Xd0b`!e*yixHp$74p# zuS#xLf#8zW_B^z4Mj)f}yF&&Zw(xiJm_i4EZcFvF~j7HPTZRAr%MdTZqqBB&{d0poednl z;kVwR=_>$dqTCq~m_*d+7&iL~VPc|9o|XLhZ3e#j^{c&NE};f zc%BVP%&UCII1VWlVOFjK@az7{66p6-Kymrhb5<3z38kc0jIIq3P}i9Nhs_l#^!4>c zic=6DI@Qb@gv?w4BrB?`cqAXn^7_)(UhWZ(Li*{<%?HJ--#bX|jW~Y(7Zlcz6;qGs z660VTrQ}4|2*w>2$CyJy#Lw6K}hq$zZ9$FxSRzwsmPoeadlHqRHR9({O2^V zf$e2L5Au$b|G}dH1dw!68*PM)qbu*5K$f&ZbR`1F#vGcx$ikhG8$0%rbhd!KRRb7V z646aG<BldozxnlJ@UFr+jZ0p)cOST~KY?C=Y_uMsOuG&x zP^)LdIlS{aq+iM&F1%Sh1BLXn*IJI)9cYf|A!F+cIS8MuL80zs) zfE@j_g@HHbam*wzVX_c~^}VDJy$6d8G!>lvRfeyQ--T(m1DG2P13F9&UxD3X(pUoN z#to+5syuFS+WX4tQh!NqJ{Vc4j#x;76Gi|^Edb3o(tnk_O4nVJ^86Q$jc&Qu2dVldW*;WrpW3YhV7dmhU?CLT_|#<(Jtv z{^Hu^B-|d>wo(#syE_aW|C`$b!+lrs*J&mtYZ*7l{&Yg95=mad-r zs^*-kn)9{hYi*%zW3rL!0bS}E-jkP9IKNplvdUBJKF3zNIEb+880#|!ff1{K0tZ#O6Ht8pyC61bI@zbtj5|?RpIQjGp)V_kH zZ1lm4+i>OKe<}~;mgw%_JVSp^opgKEdeA?){m= z%i;rf5tnFO^;@pVm`wa?>)LXwI?w0SZDV(2XCHp35@{f&;arxvckS)|@V?g@-xWuO zPJeHY71Bwj5j87S+5Gaw8FB-)aWV0H`_~_TccUkNN%3)EQ3&ft+&5PwGL z0Bl(lof?$bhI@?)ySX!f$A|?vZsb24A|}han>Mw7COU&PLPQ7Odi5cPtvQBj^MJ2- zIgFm$uTS#F_ZVgsTzGkjso61_!INq8n{B+8HhJ<=++KPKim|nOq1g@_;k!2e zc$FIgb58JI>93FhBS>TC_VN(6{4oPbldd_qyRM_o$3Cca+Me1t%8vVT@y<EY~i zG$+i|aN4)`>~}J0QQs74&IfW_Q~d(U5+Zw~r>+V$Uf)mN;XwF0fovh%Z<6=DoE|Z@ z%Ewl_lm6D@C{P7~S@L;hP18QeTMy}3l37Xpp%o+)hF5X3&r1P2rr@D&7Iud0V(MCd zC*hrmNObR44eh#8%8_#)t7*3J#yD+qwF>WJdd<8eC%RP*xA3~$QQhj+)YzJ(_Q}xK;*Up;7^WRE)ZrP~^}hD-%|Xg7s{=pCT_)|}4#JC0 zuO-klWam6P_muFy*61jD38k*W1?MYbciRmKU(tz@(>dbDeP59o4*TNbn8v3c?`&LO zx;{?ZtVb^^#^H|-(2_;9`-;vSCbN0aYBEL&H{YOco;j{N{O;6B$?2+_wAp0H>7-NR zmW>Co5klm3Nh;Wsrfe@X;Y&6tZ0L#8+NxTO?TJl236RTp90q5hOPQ(vAk$&<)gqLf zWyLz@uCrSd{-M%|vBcAf*zv(;l(_H1;Mq~WGleGN*~TQEy5||WdcT|4ZN$;nd+Ns*|>7v_jf*J>}}zV+6Tiqlz*)S4KTQ70DZm*PZ^33w(Y*hjQ2EZE-7^ zGx2}p_B&xfcs+6;^0?*H_hl-tZSS}h#GJSEDEMmIYIW@&k`xtFCBKv2a%t_B@4lVr zSFsc3ka50JN zB)B*KW`v5@&^MY%DcY0*=vai1rib$S)nmEyLTDDSA2>czGUmCVR@DuquUT%%eL6i3D7Z|zx}o1# z;VAhyjUdd-hzeCce=eo+19M)P$8!JP>e{oR27WI$zXlCko))bujc-!H9}oUptqH>T z$8WT#w|9yJR)1mK_z!XsB)mZBsi4vFS)M59#6CAMpv+>rn92)TikC4`H$Y*c27OL+}c zp-Eu6ZCjh1=%$Rw<&di7M&o6dRR+}V62xNDy8XxFY<>Z1RYrmA@Li(4FU@$3>Nb^# zTNyFs+WpTf%?Fx9x5t;AR&LZH7$*zlV~obh%Pi(TZdu6x?+_CmiO9%#QjVpK9A0g} zACqC*`1k-8Ew?o2N@zao`KU6ECuJ(`3!;Y4c+qn z1grF)AIMo=1+xL41I?B#=%rkE4<0%+2mp8973DY_;Xo33iKdr24|~qObpuFJwaoHG zAwJ3D8^^>OQYEk7taidU#2!09CDC>=UUBpM(rIwB1Ny5(Pp&t`E9mnS*v0*guTJzpgl3#oWSzd2wlJ z@W|SaBahP3IJ6B5Dk@|F$Ve~c`Yq_^&!5_H9~`!{cfS$iKFicA>G37?VFR-pA6Vr> z#(TB1Nwk=l)H0Q7c=)Za2<=sM%z+K#k>`h6&=H^BSFCNIEhs`P`u}|)ZAN0zQ>RXy zH#cYWxOS}|WCSiNDL-EbKM{TEuORF8PO$5VFLv3w(*Vs(mHEH^U@IP znKNf>Ay|`?l6sVpk#XC1cig71XnCS^^jg&(QNPKqC^^c!aonHQi1t6S#!624_=<4s zqShwnA}E_y4r(E+PCKOu#&39^zt<5fFt+?y{J#V?yi)F*(hL+C{Gjt8JU%h;C?$nW zQd07kXrnjTT*&6v5ahv0C*o0f0r%IcPnOs+$Z4?0S4CJN?4nOOE{VsgvWFq*w> zC(rt3tM#&OJSdhgM7E}Z=VIzE?PME9JrT?1OS}H3(5Ntm$Vb0?`O*P~1ShuxVqqye z1WOvraG!7@dr|J8xau3JN+<kEm6xIm(CEZ95D%X zUu8l756x{c9sA=1SFBrFWwBEvtk>tZNIU5adFkvZwV{$;@|JCUMluvp-M-6UOp;?o zMojNz*2UX*H!bzQ6bG@=a?YdxU=#{IEOW5Dy!>r@yAxk>381&$12Xh!QISE{jk{3L zU;j>8=TQqXmwh`$sBqU7Y;2k6_}71*t_^mJqxX42uHF7pJ~xSi?_-Ao#lH9|y2x%k zO5#x>q-v(rC7F>Th&`v}#H9MD>vrMkExYhv-a?BP=kM=N$G~tGhr`)0HA9nC;r#jY zwjZ8yE%l~_VDq0cY({5{d3h+``Kh!j`Aq(ZZ_cXfa!%()tGhSE5%-yFXnsOq#JSn$ zj!3&X`L1{$GL>W?eUPM{|+h(Nsdr$gONXy|NVOEK|S< z8XZ_7Y8796T3OYA;BH={#*{6_2BFKVP28b;_pwpF+&7ZtkdrAU=a3+E-9j zRlR!kDv6c;hg;(Un-7qfSfy=hFDKXjWb^X5^%4c6@Zb71YguTs_*5VXDEw}raWbPe z+G}*vRgLfnVxsW;lhfP-fu0Y=`}4aA+5cZgW%GnT?5{r&4SPj`0LzGQ{2rQ{o0E@z zR?Zs~zacp3=D6j)!m9~qN-J}|$FDAzlO2xF-OOLb7_~HBG}WBlKhV@cN$Vt*x40*C z{eS7=t8`y9EnBp{@=Ny&pdcIX(TyX~T0yl($hRicoYv4BNMG$1l>eW?yI<8jGi2obd&3?W*?#->EoEF>oP2bk z7^Y>bnuMzwx`8E=On&e!RJC7~kA|zhKj8(_<;}C8;LJoE=UCP7{w6g~<5_#`xWa(W z(YAiyg8`*`8G<&1kMsTwb8ILh1Hp*>mjG?UWl~1bw;@xTgV3CK+t$XMY$R4-O9+6) z$cKDM+vsKlQ@Nu%r3YVa1WVi}645?*W$Gv{ol>DZ*H;(2e zZ=L?pfu>s^QPC4Yx^H;v*dA%Ft3_fV5t+cZwr9#@*b!P6-+rD^@`?}9LXS3$0SK5D z2BkwRI3qU#AkWW$mT`bKx2@dR6qLsA_W!!u@7D#r@rVE)12+N|KtzcU_HN0iwFctv z+C~qX{i>#Ya%hz7#BU9?w5TFzs0sz=?(_Tmcnl2Rn@`JJjS%Gb-1P9=Cm4HU5{v2rJDI3Jxl+;} z)5DG3&3+juY^ImdulWpAlrOHS+Ue-gqpx5L;xmkE!?e48Q;O72f=X;TC*E{9GONSK zyriMmNF^g9BP1-020(EDB;5*&o;KI6E&2Sc+pH$S1zzC``8KLK-<;CbF)fH)bSk~w z7!xv*!*_Wz25&Mk&$PxzP&BEr@MeMRzR}&@JX`VRWfN^O-==X|AXTd<<|)gIS(fSI zoGB<2|1B3Pw8eUJjk*iMzJh5U>bORbq1r~%pHG6Ae8nfm$6;!YN=RDz`duv0$b@F9 zTy=3F!|E^ky!HO4PJ&DSFAFi|6grE)6N0_VNAhx05V#c?^|BkX3{o^ki6c|V?NX;~ z2$$APrt8Qla>S5U^o$a53a+&aGY_9<@z13w#8R5;rc-pTMpsx>CQX z7PS;nnZ{UCE|IlQock`P?z>z1{CS{*aoyG-LUFPl<^Eon;pjetku;@Fe)1EMV=H4m zkzA~JZ#&tuYBT~LPe*{f}zi?>299oVxzV2Q4=NFWEM0(hK`rOitbcWUQV3$#yES%$YlH)7M`oCq#@j z-s$Pcrrvd+HkP=KdCRE@9>X2`b@P}XaI?O$%{(*0)KKPlvF~P#FAw?62&<*n-U-Wn zy8W6TlViwqBAP%iYJt4yGXS%Tc4Je$kjbLZYCITrJs~GA)mMF+8cr}fa=s+VAtOL! za31(QAXT7ZVPOG<2I8hV*kAPs@&|dvq!@x(_<$}p(h~6$AQqgyHqYLed~If0&T=un z3VS(+Vcosp#wqG=Mp=oDwT{_1X1$Wb4$$$i$KqWjQ?Pa}xCiV_zDCZ=Hx3f7e>^8J zf8||VWzz7FH#kQ+n^CCRMH6AlUQTyAc+;T55ht1u$^P)ytO5SIg2%8Sl@HT7DD1StHHGo(sS=Cm&6NDUjSOxpxFqH3tYa zcgUGLrD|{1RLKRjqDEu*{qXadA9mn<7inl))Z!{TePhTB$k%p^1g#$;JkE5QotpCt zaf~fSPpsLvl!7-B)4uPw7KzXl1N%#?lq-Lh9KIS4;NbGy%;BrYoA8H$B57DpSm(X4 zukg~+QreyisWGHFOC4#l5qe!}+goP)5P!#K=dRk(+1Z@N^YgdE-;^fZr&U*(nhOgH z-vie9r+F4sa{>-OY9T~3nP(XvM@RHO64#^2={`uk-@aijHb9gYtD|eZ-uS>-kW&x# zk6X;Y{-MHS4{7!x4Xxt6*i!cCrKy=ayy1mi!>+RdyDdk8ipZ=|zd{{*b#yugC9^5a zBP?3K2AXpk5?k4eF$uEWn?*9jFyTH>Mp;EP<>uz{-oX1rW@uwwU0o+X7HJ9=xI735 z{q6BLtXKi{KVv&I0|VSo!R%Aard*rn6Hoh)yHi|9#oep@Ry4Euglilj!X67ZDXqo) zFrCKfU+l56iTGQhGnakER>C&Uh}(d1pFt|6ngN;T^hvYT<1M?OG<|4iyxmg#d|8^~ z>DdiCwXXa)V%n(75lz$$wwEtA`{1qm?_D2a@xhbBtc23v79>;daGSK-ck{`r_vUNU_JhCkx)tM%R|g#ZMixCztuQFk5tgQ8K~dL(wuZ_dtJ5?f9TB zE%UJCqri)zwD+pFVLy16QeX%2XMUt~E%&#Mtuiz>rjlQ1x#%*vS?@q}t`Lk7n-dVe zlh$}4xl zr#sf^=*I^IPGWO8 zhp!Nillqt#!M<(7IZ`T#FULWA{&TAT2$1Xrudc508e&RP#-q-jJsWDO!{`M58}Vi~ zF)8LFNYin>dkS{K4@Ui7N%j2*2C&mh=Z_JM#dH6vyJ+R2xRTvrisXHPrRMDHY)yTA z{WF#5vEzpOWPh2LxST~Alw0a2m+@^zjWb2sCD=6kjg1hy1JkQBL0*!I3#8P8@n1`V ztaBf}#K&hv4RVj&+f^zx5Ts~@97VV9?OUQM>9gh)f6W<;f7FWReQV$Mozl?>GsljlwH^5VBpc$- zO&t{#~QZ@N^yL!VqzOn!;ea_)w7vT^^z-pyDrEae1!r3 z7^C-EjYfFau3c%nFOKL5LzMhP1wZEOsQRJ_QNOzwMJ`yTgP8d z0g^JpA*iP`17M);Q9*>X17T$7-tmM`fZI&V)-N%IB7MOg%caE-_UCU2{;`3wKI_6y z@HY>eK*|oG@K&N3lGwuyv6(8^0Y8piBJCAO5o05CZ)tm@^Ow3L$`hr6<=kZl;U$tl zhkCNJBpWe>We_~@Z?PsrQ?z6!zHZa|>n+xgJAgy5og2Sr&mJQ|5zk(T)IB1Dmww#y zmiF-QcoKlOs?-&l0YA|gPIEL+ocYx6U}IBII|~18t?oT|u%D^ zaR-QwNvxzZH@W8i!dI_e83M4>7O;)4^n}UhX`4)n7p}7ZeVkQlMA}L-0C_{;fM3SO z{6j-2BSbAxmWTd+_QWDD+zt@Vt1@*VA=_bjMsX@jso^{_HNSh>FznPc->aO%* z^7sR|D8$fSvatz#X4BP@4#IMrNQB41`HRTbg>U?zMgGs^?%G);Cs+rN4|MsL?Cc&B zlbzqpt$%+o1?LmEozUY9!G9kcTsE$jM(Ox#unu1)CK~1OD=RDFy06t_)E*+Pc=B0p zE`M}Pj3q;ZS{-GRNza0d%ij+T#{&{;T5Qh*4Xc9IJ1&#&nUXP7N*}k3-qsH$R1_^w z>Hhd)1hYtyPKK-I#zQF>Fb{5En|f`FV7btHShwlYe%8ijA{K?mM44mtco}YP{h!Sr ztd;(w3!<<);$P2do(nSK6jAV>#lAp(K~Yg^4$b(Wm8JRqCn{J78*M)p^0nP{fyg<| zfKz0TiJ;TlG_zg*`ua})Q7VOM(?&$%^x)aQ>duKSM7Kn2EOYPvhqlOdk|diCegAF` zp^pG0J^Rx%;z}Q?U=P}626uOMMwGwn>^$k@G*fm0KUVC6*DDC$ee16$O?Oz=n0?!> z!DRMlgkcf{mZ|xm8P)B7>1|X7K*7{f2zZ{Yn?io5N+M6z8Uk{nfSWe^BR{SvBR$nd^M74Q7pa<>TFsX) zmv-;o-9ym^P(-K6o)QsoNEw8}{U86Q9f6G0`fLRcA2K~Z!0-92uuu`AmDje!BC3iLgxcDcalk#H;oNN{3%mi4DqUv= zMDGJ%QJ{fdbpv+2BxnrUE;mvvch!Fq(aVm_0J8;xq^oI`z!wpEl z(0>naINsp3+PNlOu7Yp~1)X0s9Z*YD|MS1BI}bfx=)#Q^WFeR#h~eoauW~*RFgpy; zPcvv`C|Fvig(!aP(jA2jALI^tXASL%e&Z(2N!~BkRKc_%ooD4{lTa z>j4_pYmi7G!H%+St$YR?esei)%Hg$p+RgiJN`^_5)t|)xP ze&KL@DNmGD_upv=5s4a`{g2PJ5Fku z;z8|~Ytju$7s!amXIPagl6l|Az<{>7xjEF7_6aGLKe^E9UuVayiL}cV*W1%_N(IxY zn)iN}ur}ZQBqdxn*`u3(eAuS92LN519GN8Ioc11(r3VHnfAA-@z>r|~URtkia(vu4 zoEz^ON7?lB!N1;?2+M5`-EdesbRO6^-}d#%w!iHDx2s9?0X5^f^yKQlE%Obz6q4i{ zgJAXkkFf8Kr@DXtzY`Uj$4V&4-ZQI^kv+n(MSI4w!|wiwv-sU_#c3SpR&VJ2|FIJc-$E_e(29_`Ss#*KX`1rIQh}3TR;<$t^^_M=VOV@at0KuN;?DZG_)6S%Q=sAXE;?15C|Fp&tq%Z z-pb+1@&DlHUzY+6YlP;D{Z%m9b~`VB!fk2&{)kdA1ii-@`mYhpsa2!;nz{M=J5l2q zG2&6OEWbiL(eLx3RS493d1MPD&q|rbv+F1yfO=I$g(SkLee*^PZV`cnM*Gx>gFAPR zT3n!&`OlTw^5U`v4pTs((`H*8@X%$`6Ox$76*bXC zwL4L{IwZeI30s~qQb|mvwR_8Z|6Ee_R0yaX8Zs{J{_;oxi@@aYkt5QEhK8gyQ?s-2 z5KUl&3V+2j+8WiYl2Ob*Q)gg3aGC#Q-~E}k-zbARH6hZS&wpDCAJ3ad{{CdsU*3UP zKjFC?QiwZv_^=XrjRKk*l073B*w73mZir~uis3%nap`l~K^H>~s7Gpny!SKZ*v(yr z^gvkpU#`}|$J_))jyW=FHJ;fNAtsa7EzJM1Q-5AOr$%6fE;ID_9*dusZTyqjf3}-Dd+|c1-QE{0IHfswSw{&!=Fh4AK_KN%^tf` z^TaiG@{%8^|3#>G@4!P?ym5q?JtETsG9)WJD&kwC{8ACfMeMh#w?xiai|E&vQKeK-a zDIcCQ(s$>*YIxg{{bvDwz3}pZyi$+RVsuxn48Fw0Z4ud3Y90qcAMWC!q8FVbBO_Xc zwpW48oELDBCpQ}rCYx)1B2+V8yD5awLTth+T# z#obFHnoXeXTJ)2Rx%I6$3HO|?*S_4fD+vF;a!FFv1kNy*4CW{b{u=?1u)TvRLb-BL zQ1B(}Y7dE>L^?NUeM1?PE1&c)q#$-53Xfq^w+<`(z8HTVTNi#m`zy?TXo{Irqac5lra%T`m6aEs@eoGWFn3NBRzs@H0N#omj(D zsmk#U{T$Z-iGYiyLC1r~4Kh?n7@OsNr2+FOf?eMq@%Jz*Ta7QVReR5e$epo~SON`R zWmT22xjCcTH!TS3NL%z?Ueo41d)735eELHU)URmFG*WjsMK3v0z$k{gTAg;@M0kI- z1H>BDloS*X{{8n~8SMDQETlIdEMi&elb6u>@}d*jS?1^FQhzhD#@jvFi^f$|ncS=_ zwwKleCxLU&+}5UEd8TO>rB7@T4wXiu-ih6|0SG4wXjmFfLR|vWQlQ@>R?+GYq=f#> z)!J&)64vqP>FJH^?Rod@+m}>Oz)xrq7!V-o?(UwKmnSJL4Uzwf^3#($qeL{~^6^5= z5A3{qdvNkug2bbw11bGrb{0Kw2@rhiLM`mnJCMu3-j!|Vg4?EEo!K{RG_?D#(X`ab z+ESqwj642IOo{*=dnj-G7pr6IiOGU2Zv?Wu&(M2n^B*q32bYK^8YY;Sc`AF=K^#5g za&G!J7hsAH$wweAoqSOBZO5L&1;)rh2#QH*-I<4) z{=Vd42~e05(h%`$N7U%*M@CE&>$(j@b|X>LD#QQZYlXGNd%J5pSz{m-F4q<&`eT9q zyzmml-+ENj58A-|?q;ZqK`osx+vInn#(z&=NC==-UqjrELxq?K zfA*j_Q%^`%rqY*wLu(ya!}vY{J|HUet+c4kAl@Ohuk#0Nk*i|bY*w*n_lY^6p1s{S zL3ubhI2cMK06IP%8yh>?lG!`nZnwjXTn+|*PUeCW^NWE-W4dH?!o;NSs(H$RpTWk(K^?~)o z{l6@TDQyr=RQd_i-@#wwoyUlEt&QL?tCDHwaw7rW*Z=n6 zVu`ndQWm$|)So7qF%Ck{IaaEjI4Kg{1mF@tj$j#i_wHS7#(9vnsKFS=i9(?!I!fmk ze3X(f0%Ry-5NY8Ndi3xw2Mkid@E!3U$Rqec2a>skg&%Z&3LtZ`oOeGuKV+AA=l=4J zthayjqoDrsvo{cAJq4%##>W! zm(}D+xRAXniT{CkER?F43N~Ow{&uO@cU-DQjtDkgQlR5??H%&w&9Ex{L$&?AmyCh1 zg*KE_fAQaM9B`Pzex@#_m>|eKBTG^!qEv-py4=z-6tG|9qG`ae_6>ZBMOejL2;hi4QL9wuTs}n^cBRk@w4KZh8`gVT{P$g1BSHnnO z_ZSg(As9-ILP$5gE~BwsllKl$WP^YF7u>-J>zltpdr--Y!G#pX;_SlC)n=52vtb)U z;=U_%Kg#Ilm6(_);PTaEYM|`9PIw*k7G@5lqf9Lslw$5L7_36_bXD}3&@MC+xrD^K z-H#-lV#dyoPji8fk2b{lXZItUo(aMr8DtgIMF!z`+rSt&s4Atvsr+d{O3FYZ`vXX! zLDMvRyDqVw$3Kn3mbHf~^8$7J)Tu`cO+^dj7EVsf4yU)0Ix<%?d3Ii6FI5N=9;^9Jti`0EL^M2&irkHzhFpnc7N62YkfeukVWN~kxVvl5T(RZSOw;TYpLXC$$UOV?0 z`9ziC9raSUamFC9)coN?q&t1l54YH@fAZoE_`*^SPHIX@&lYOd?{|j<)2yEQUBA$# zo`*pJbKRY#XWiVP8{HUCjtm643+lTS_QS?p-xU}BSU#w*>SwmWRb%g<9K&Be)%3x zHM|bSZA!Yj|3ZYc`RZCzmCXQ0hvPf{mso}AFJ=|K|97%26!b(1U!&>}I%W%<(>qSB z7#fLfhx1MlZ~5Lof9FjWuIh2S+5Qsk@H!r|1``r4{%aqOGTjmck|%PL%WI+lQiuP9 ziX>}hl)>MR#nD%ncD8l69XEi^7f`+pgyHN*xAr&x_~3*j*cBGnRr}px$>p;+OhHkY zD}PGFtR^ntjGd>g44+Uj_wn+A$%m$$3-!eB6%3*43MVS}-{5? zm>)C(svSlEd_rg$h!g`|82;J0SXYt78hO2i9`SqC-mw0P9$dbCh2igY<evo;+%+G(fG~M$wHi*)(RRg+t7<*v@awqQ)hV(Fp=L%T4 zU-fkStB+0OD0cw|KlY~+7g*3Rolf%BAW{5%M8z@<@mlYk3wAGl83=>RP#)U<&ORSb#I?@g2@#;caC!ZVM?Tx0?>w_ppo(> zVZz_L|LYf3XyIbhToBp?w%&eIPM9oaTI7<7#c~i{U0PnQT3wtx1K!uGwhe#*EV@yh z&hFFbd-C1B1NQ1BkJkeE4AA7yEw-u6!FXD;_*qP{Mg7k03`a6!{M_V6yPu=;R}NOp zehbP_dN6VcX>lU70&Q9@s0ornaIZnvsF#=ZV-huT&&=Xe`8xzqZo zWB}})FEn4dE38T5^h(E=q+B+&gdxf`24K*}Hx>+jxITQ;%>*Mf`9dF zciT>@@y(l@Zie9swY$9ek2~DX@n0X|*vw2IOv=Z%(DmqiP57GuPf=*+Q3G-k{#6~(-M1b2Nf7RT;M$D?79h943@XkjVaKWaOG}QI zcm9!A2w;1Uz1uJB_-?Rg1b8Ws>)(ljtn63JY8!Lj?JV${$zaTKNj4N{g1UNpGgbc{ zje&9GKcMi_u`Q7=zyPIFk3drkBEsff&r6JkFf}da@UG4UY86IK{qQ<;bN8>oSi%VI zFZ8#?tR2cN+A?+!ja@P{JjKSwCLYaKKyz>hWE_Shmi8|LcLCpNOkVa+YlF{7Bf=Ga zNG$sC<7LvT>(l)VV$Uf!$}kC$@TS^*>@( zwMYhO3+ug188b{;of#=H&QQQbBB2!!L{o2F)ty{$Re&Rq~f|b!2K2LAi{i zCMKdZcK5+TR{65fZ`wE_Ve9tlWAjt#&p;)F`tP{i#6r{}OuZt*I=^EP+vVd;>0smp z3;XUsU8r6lXVj0`ztob))8C-%I<&RGW)=h5#Y5%_P>}wuG;O6UO!pZQ5 zZxD*c0A3!XQZOs)(%B82slat)`W$IR4CXwPQ-R)da$uuxfNEe-e}H6Qj%pz1L!X1k zwv}V#f#b>j-B(nn+3c^MIZFZ4qSUCVCy#f{9TL3QxB0wX%ei1R~ zvMVobS{ckoPmSQ9xkrWfF8y`K0hDhsD~?|uV)z`tIUa8WQ#$VX`kp~=)b>moEEhou z;eJ8c#&ix0H5+t#bBslUdL!;KH1%}N?t@Af()*MHa*qB~%3Q*m*58+`=N^=Od%zHH6Fy$W<>yXj9udiEqR~4*+n*s*swu44O+#KF%7(mYMZMEWD|O z@#>7iO7ilxnac&+iFN9}kor2*J-zfljn9s?6${5t7Skr^*gyvGWlArdhbujo^T?eQ z27U%=&szC@C6@7e3sLQ)(DE}5cP7G9{h815gK!?YtQE$u6@JSaSk2Ns{FP$h+r^gy zNZ!O^xFWXznJfg?;pmSs)-cba>IE|vCKEa%5K;QqQ3y?91Z-$vreC9^q2a(SnWjPBjinoLX&)d4x>Y_?BZsu4bCHbos06+!`opXwt>sb#Nu5Ug*u zaBQ?3T1)niWcua|rs~^5swfp$8)oueNWca``u6qAw{-4n9-BbSB&PFr$=$Qw>A9iF z3iJMYhL9uwxxjO+V3e;PdR|OPtOmZ^C~+~#tbXH9eIeC%uhG6eGxOdI7_y+4l(_xb$x)aIA4p2G(ZRse8oC7D&^8#VIP z*Yle%mSPCOz}vANi;DY21Y@E)7Ixc6D7aqPLI+PM~g^U1;JB zzi-o4-fE&*iBwM2t&(lK&vHb z;j|B{;nItUcTo1roi7_C!QO{ypr7x}r-mY<1#4DUk=xXDHvL(A23(T(;%p8zG@YVX zhe?EZbhU@Mxp~dz_X_28$@S(_8_oHj$*u++7O&^1dGnhKux$aZGJp?vR7;ZBadrRd z@R8q9nZ%j(ST6pI9M8bhJ`<|uc124AFygVfy`3+^-LkK+r81t`jC9{kH=vO}dv>+^ zN;2oCNzg}fImCl5`~d^OjNp5mgNB>Kh6wwS^D}cW6KfINM{V@DLSk*3FLClYhlw@^ zVN-MzrRY>Qb z)(eyBtizwzwmp$bBDiQ0A)BII@X()#aL0>wmM=*AjmLDS$8^!2P2>)+Nth#E)Ip!G z9GSV)5BsOdq5kmUHCussjc}f_ui;odO~FNo*!>~&e}cIV@!||Z>7IWwK#9<)dm*#v zT4FlBau@o8a}7d$gp)Dx>8qplIshp_i&5pW`@r%$V2B|ybDd}7Uze)g=Eh?h?eQ4a z^q|}PbZYZc_Z3xvRrgu9fq)}VVm!wqJeZ);o82?2KkKTw-Y~n~5b!AX14RlZ$#ag( z^B96_YPj?I$g82-x?ZYNEoK;QSiYV>WBq5p1pYs{;;1wKnsp*aE1coW-~*0IWWA~YP|0m5Lre$~f|QJlL6Q(zjz+q= zhWAKTKu$^Ha|E2cbyeefU86}mkyXjFn9XHMQ0})^R6k#5G~vMSa)+4s?CH zB)T9WN{noI93FS7orve=Etp0$-d&oWGz>k_W_)xoNLCm%cjxh!N3>P2O4kjjRk{FP+)>pCLSMyK4o^ilF z04F$56DM-+iD4xB;L(_9F)94yRO!^zw)kI&2!y2%`CorJM8dZ(c31x}_-Y_M!4Uit zh@C>pb_G+{2)Mx!ZcaJ$xPNpf4(12in{m!sVTND!Tf#VBK9+=K+&s zQ~}~>fOsG}X+ylJ#6tC%0d9S6VQlpYYcFA~J2 zd>PNuS>1R;R`%`Pw<$AG3eS0d&v_U;O|PO^KfU%{gV{yRf5Gx3vFg2~u+!wshnPch z#O0cAdk4u_a7*1KP7J13^bR^mo^%04cZk?evdll_d(o2X^2%&UqsZtKk4WpN!KAkR zlGFNBVyfNjWNEo%>I8N&-Fa>>=bl`(O{{K~es(yBiAD&5Je@+i>ND0oRzZAYI$z>i zv8Aib>{s%ZDbK^q@{UCkriEKi_^ci`=NEZwxLWlUvDBm;TElrI z)2U%HySM=v%j2b$Jmk%}U0^F{5+oYy7E>b+{gDDf#P&vys0t|8M-GHttmbwCW&M*l>y{;t*%!#5)f_uwErZcK!<{c^YWX2<^er^b@_chdo}QN9v1p~Q%~uyi9mB+T?rEz{f=iYHf|-8Q{uzy6ptIZz zJFSxWWT}}xnSl5J%?KAL2TeS;k`o_?_->z0BVfjgkge!n&u15mTlL9)3zN-?dM}qu ztKWx)yPRY1Ubr64yWZ20sawF_bYeIqeY6b}>R=MaL+Y&)<3Y~;%x!_t$-P@e^p3tq z;lM~mUPP`8q=)aV!0@qhW-aE^$+!zXdcK~XW^ zaxtm4t=Dx$42^}nvnTB>tKKV46}m;qPI1w#$_IbcNPY-{d*IUchBCG=)rHK*>unen zHqgnb2Rbny9FJ9NaQCZ(h9}n4bJhY>s9^%eoX2_>&iE{Cg5!v`YY=4kq6)$dLnyy$ zbcZ;{GwO z(IA0K*?G`sO;DSIu#in#l4Y}VDnSuynDnkUZUTUJ2l?9F8M&@QqnLi*;_Hx`Xrl(| zM4CJuO%Qe0uU|Y^1e(HncKgVcuk)7q-%god$JHmC-nN-uG4h~?-f-S2lXB~J3WnSc zVTB5Op2r>U5I#&;x;H`a+JUq?FHo)D!Sy^uxx=vyD3nDvZUB-kF};qVt)d+AJf!|f zWALN64}msLBMg0`!1sla`AYq1u8slE@7K{f{RP(Xi-YA*h!4m%M%T2vqO}3UYPpbV zpD(|-j-k$yYni)rC_GSkKTQ4A9B!!Zb5W)?99^$?uFL`C+5_H z4hzw5Ux8mz@jta@Dri13aE8kF!;L$ck>Q{>FfieEsZeyGBDG>gK-cVe=Uf8hAAl%X#Ha;aJjxq#JLs z&Lhx|ecg3o?AYv*@yqXUurdcgDd1 zZE2r`meY@#F`uGR9HW>ua-aemQD3Cgs(slO(DRWvw;3+z$avu7r{OmIaX1sBmriA{ zCtGB8pscKLW#ox*!yLHINKgsvXrBSyRzB1LVF{N+Kt*s#O}pKF4^fy72gf8PMT4jZ zq3)A@9u))aLgisc(e*D91B)!87^64AwrX7bihH+RE9yof(0Ci=*z?cU8%Ax18}`2N z(>;nhzzPTB@_yMxo8_kxOLSkrA{2#~<%CpqSHraEYhgnU_n1!_0tmsyk0B$Vtx%V` zQYep+%VXZVU$$48LeK9!S|Jd9MSy7D8YCz5ocbK_87}9Oq3@T>RZ?V;aU3`xNptht zmn-uyBvWfS*<)ewF6JbMP9CF$ISfSR1$rzG$YI)l(1QBFSwuZ)%&z1^5}BSV73%7w z&l4~+)pYxopGYr1#DvwhYc_^L_t_KB1oWr%z$g=Z-l`b8&8=L*m+|fdb@S)vdSNb= zyFTOezir9`J|Zhaq1iEhF^ft9pKvp7Kl^%wz+L0DvyED!IG((73XW_4tw-l)Kr8SR zz5PL3f?;mPoJbzpsgO7F$1F;%aEO zR<&XF-C}HBq@V;%E22S&>cPlhTHDo(+JS{rc^=#MX%buZlE9-oc!tK`gK&}5P0>d{ zO>jzPute**eJrDL8cx`1@T3(CsW5hsU;W0@c+NP^dC)KIvy4_ID7s8yN3sg~zt%sf zq0$(XOpVmJGXsKZXL(Kj2)DRcwF0_dARbz`MDYTrgTIZLWi&!S!KmG2hL0|o z?moy9lr(=QI@(mr57XMX4c!;RS3vDJfZ2|LOuq-zSeT^OP_Q}ZZ~L173JaaMNxp!8BjOLXK z^>d)ETwGy+s*3|rDvi@p!u-T+4D_A#T6ap#DVDfv*aj7_6sf27;vZ5Lp5~QzX7WnQ zE`Ur$_s($U0lzyXG!kx9Mf=07nxm(K7*)%Ap&`U~_}K75;pKntG;JhW$z+E%%*7?7v4%Joz)ma5;WB%UV>?a;>hx^^o@15^UVo&(kY5<2zX_{(f=<08QSt#Q3^1+~ zCg>UJDs?XgwJFU^$sGRI;hdihaH(F9?4f%dbLS%6>#%SBq#lC`bY8$jwt%9!iMr1U zSoxP8M)(=3_!4NnVyD@-XY(wNW${-_5^Kg_!siip*5EPdn2&GkR!3`>-z;f6!{4X1 z?6X;_D&^c~=J-`K9j}RetFztq%wi$93neMu-u=BYLFK8aGZSkcoh1ZChAD^MkCLdv zV3c}RFmb%e9jSMsp^D8Jq3;+~o7i}R_DZ6^ve0tEIUihORCGE(y2e=-XfmPj<-=A6 z^pOD9K{YKd8zHnrWXZ@^d*JyvKg4{LK>Y}a%T4MpKrKWus;`B(r8s!Cl( zhO9J#IJRn8hGib+M~lmC^Dc!iAcCoS?qLF9O|8o7Pu>&cAND)$5G%Xz%sV3_>*ayk zZ1yaZSmwG-;2=Ft(*k-U&0v&awRqN3pwwoQ9a*5cjj8OmsbwpOgwjgk<5Ul;j?5fM z3)Fy=EnB;*`gPs*e7|00v4%$&Bcz1h$ro1Fu=q7#h7K*$>A7fz7FaG$owCyD%tTE! zd?Hj6h~d)_be5(l-6c{UXbJeOfN$NpUmF?MZG4{|;BxdwaM3)FW9bz+qxzR(o^cs)N;jsO~*9$H3 zUf#+5Zmqg9no5rmCdF7=4PD1L>*Tr7n#nSVd=ej3Amqj?cxS<#Iu6j(yIPA$afLq$ zE6$R9gA_DA6yuQlwbj4Aj_(jGLdz=MIwPWM%)0l)dcr zzRzaeqnwpQq4}-*&l&OKiw&=5k}1-l?*=+4bxUkH@ug4Vc+3JbV$~$mW~19H$-!9iBgx)ue-ggZQ+#&1 z)t1+e@2|87W8D>!)bT&X6w(i8 zE*U3bE`UN=*ScR@=^2WE;Y9zW)Zrl8*!meR&`sqrtvlYyExI`@5l<<_l>R2Re~}qtEP_G`>0%yjLUAMEe)idIhc3ZxDHBt4(zqBDp7W6E6WSO@jyqaWhylNCf!og@Nrgs-#LaDKiX7J~wgo0r_!ji5sLuh?<;69BuU5A^2oV zb>}7u)X@2bCW*vH;NZ;n9X$R~_))hD-Yr|2u(xJHG$9mecI+ev!ZR!YDq*vkhMA7_ zurB(wh`Kym^Yx@pB!2stN~6d0f>&MMMOBXw(vDD^bH14{A2(4#%M+&=ldd-gamwB! zOwu}7XI=KBDk>*E^Mm-@1wa8^orh|+F-8-6ay576Sq8SWn{O1+?cR6j1o6wm)+d?1 zh=1?=Qo1(710$d89Og^!est&va5}v6O38SG)sReU-oMo!9lihL4Ok;n{7>p^8eEVH zrro$|`Sc}!pM9uRHX-4nEEjcYi@u zGG%00HzOp;)#BekXQCWO(OJR+tLJq1#+_eXO_~N^JZrL6J4fjL-884`u5;z3m0?4V z9~jY#H!74+pEj$485og(*i>QcGf$O>J&mwX9hMpIRfh6l9hhuA-L?lLwrOE&7z1ZU zm4w?II3)>R0ekZf1U3{a>*;t#Vdw>d_$AFPXjCewkoNWUwzObtHJc6|26LZr>iOn< z+g57pB@POizjun39|%^49R@%Thht&+eU!s-bxK|RxMW)9Tg>U z4u9WVs5E?QXb|+XMK_N#Q;x;Jn9023dA}nX~lhFmDZda-Ipl!)nQ!v~ZKX9$< z#XH+7`;kHWL#2?%URlu|t|`H>5p0(`fLRZr)PCwSquE!`|79c}QJsu)8HuttGP}0J zy2u=r z_D`z}a`ypY$xtiX3n+4Y(6Ppkgo6spl4wSQT+@f)k>SVXj519^qMDb|XmufzmQW0< zdm`jusm_X-V%3xurrlKiYJhWA3b2~Gdj5fHe%!`BOV0Ito!-$7V@;=LCR$xTiEX^F z^e8*e{OKD|61)#rv%LU}H0S1V{dw1%D`1E^ z>Fys8?TB2O13#uUT zu%pk%7y1gmQbYCNf%Za6kG6Oz(D?dg>?1)b*=(ZtD*lOlrPR-HP9pfcq9E7XS%tE$ zkK}qw%%;<}Sm-~M{vnRaL&k!C(#(NNl|=aDi|mvL8HaNvGxrxHU(H@{ppg1 z`*Z9c8ia+`U|y&hIh*~M11z9zGLnnG*2T|9@Kb#k9Kj3B`=k0KTrT5NP9wU-H-=M^ z-T_0P{sBj+`M$%HW0ooFL9G|m_m`~bxu*;k)>Jbe8G6m(z#8&k9OA|6i#ZaF)cOy= zB$ga9;gYt#QwsTGGbQC}( z;f6EC+b(}L?Z>P;8MO79hVJ1nOXOncZ4xN=(y6MU)$t2Mj)_2xwNVdh9N9w%=6qwM zA$GFcH$*9VKElw0%Qh^`IbYHb1?7*x9NY`Geh(OU<52UcBi)JK3&2h~<`4;Q+iol1 zaFOQ5-3uM70Fh~5n>r9Orv6UTZ~t`A(?q@LA9|!|gPtGj{W^Bs&?>jb*cYc3xcl8DI z;Q;UeFRh7mdDF;*ha@M-T#qHdkj!ie7)y(UL_oR#^#K1J&2Nb^QTPl%m%v~!o~FGF5eScC1FMzsAOk*;3y84w@X)rKd`&!9gFCtOH(}Nh8>Cu>F0Zph^a?i}CP5~_{3QMxP@X=| zf^S;Pv09%fSB!Z@!3z;B|I>-8hF5@HD*&H7>F}qlbD5Q>v>HD4b}q99$}?bghi1ms zbDc!!4gz3xVpR*@jH@egEv8UlQePD$Y|Byb@!YNdDT+Pfpylb+JEr)WxtmtiEdg+B?*blmHnen zR&Ri(;2&Cv6w}K0xO!3RXO}be5bxcdPi=ua1fy?j-X^u;S!Fl$A*9O=xvBW>AG(py z`B?roN9brv`BBI`WZye!ay%_GHP|~U-?IJd^hCtl2YkMz@*2&e_yQ!%ao@(CKDy^n z45uNh5w_Kws&%;$=Bhm8dp-HeUseRVg=`jOlXJd4ZlDRfK+pr=Pn*LbS{9l`yM_Sz z8hMlWoK@%9hw$S_h(Coi3hq~x3>xxAPr+uA-;VciPQu+R>WiBy$18%z9HKO(2EZ8^)^5&7=;L zsIw=k$OS+3(&bwSZO_19*4bd)H77Z(qg!)KYHA#eI?I(uJhYe97i+Z=XN{Ff$ypejDiekBXFl75ua)u#?3>|ET!@dJhCmLliljS8zVaz#s-<$J)%$XD4O zC)#=ex16e@@89Cmny4x<$uJ&Ce*G3&1ja41X9wuspMWeN^7B($-s$dqLhf62XKPYR z!22ERkyS~XojOzKI`OL1>LD>IjERG|?V*|v725sRPtR6|iJ3qYz9q9e|uodxgL$d!Q<383rc!Cf{w?MmD2Lkq4s#z~KYh>wY%jTHR)v#cH zz+TaeE?IK#P(>+1Lt%o(l>tr9J=mYY^j>}hHe5V2mx8Kve7~ql(=RC@>mqo%JNU;r zTb938`Bqx?R_r_=9?kE)azWCY3!Cp;`KYa9w%_h$d&!xCa=?ru0q~B3@N?xb-LhOE ze|Kj_Hih+9GzXJ;d~-QRaMqeq`?J_%OrNsM8W0#WtjQq9$Y)XEs|J09U8b5$G1SuS z{>3BnW|MCyc4d0QwApGm{E)x@xj~AJhCs*belsoR2IVHMq34t5_|s92wOQ&M7e1U` zR?|-%4*=C2ojXgHo?et9e%n1id6lZ9BDqZMz_wUsTdJH+^92S%>y|$Q2!(SZ2<%vc z(yDhx#J;s6K!QAjI28m%P$OWa(Fd& zK%oE5t7?y`8BNB8dlWY~fgC_k^f0-{rB7^_n0!*i?`(R(ZEUs=Bxld)M57l*-1mJU z^*+R1rEL~_^+xH6sXX!Z;>ya2)N`--2Qn!KKwynM8@5J@5X)Y(R`3Lfq4cLfY zd8OwSu134fU+y_z;t#e>A=vcfwgsZ*+~XGw4F6$E&&jNUBE7>UcbR0FxlCA7dCH_d zluO|k(^lGCq&a+Drxy0*_+#9>j7{e8IzImlvFTZq zzO+A*L8Gn+#nXAPXW2eiAMB=ZjXOwjwhm48VRnVKQ{(*psiu+?w;u8w=6$uNwcJ8g z)F#7utZ6@4X3huse|F6oy?aFDz@cZ8m)bKz@u%e*34~u5J+fs6o`7W=^LP<F^`1u3)OjoG}Qa!Pb0XWS;l7q$zX2iAlF^pt_Lbi^mr3U0k;FgjpLU z!+Aiel#`#*hROf(CKGk1d9C|<15Tb7`f@VFA}uM!mKq%>IZ&4I=4t$ac0pePCZPwh zN>9$#w_y!MqpoVEIw|%M+;wq?x-T~Ybk37Dkj6&P$bmg6Doij3>ToN z^ZG@$%}61n5M(`!`WZa-j`cThB;1KEB8B|+y830Pmh+zA8NQ^>+DS1t^ptNFuA_BW zs#>Yaio|*8DS?fJ*1f`qN0aAkb+(rkrjWA2l89*Lwz7gtpcpNK5S`E?-~-)1N9&vk zwm|%~PM-Od!l%_2>=2#?*R>1JMAXY=(W>YT4WpO9v+Z;eLAOvr;x!p5=_yByp}}H; zTth2Sltu8dO9OslJamO%LzNDAhtPH&F zK5c`_ZpmV&;R?oM#MiVrfazTdK<7;uU+tznWZtca&kYMBsfZy{{!j9!& zm@AR%f2)#cHBL=jX!xnl%WLFX>LezI1wS(_C-MOi{Cob_E6ujr7b*kVE=q}be*3il z>755Po{ZLHv&)}RTBVB}x)%>WtKoN~*>m=sn%Q1Mxlt zN>h$!M)ig#`++g0X)^$@thSU0-D*cEor&mt9Z#q>z59-psX&fi@B6Xsr~|Ep7(O-r zVVG=g8J64*HHMW@0%K5bDI$p(&0Ce2pI^c3@pb|MSBv50{JhobPD0u#3ag<*vhJ-8g*kero(vEr8LWej1%I8 z6EHaP2S}L!tNer^I3Z6vd>w0}796vv8_z6U8^zfbBw_eWE*y38;vICVjPj^vd69yY z&`4+gki7|-Tc>(_>ZB`^7{`8rGh-`FJW|(VImBhASc9L5R}I&tli?EE5}?~JmkrJH z58N^amfDDp@5~`<5<~pF4THR(1XVi=QB>&Uv!iGx4ZJJXW7?<5WP}%3SBFBC#XS!X zIzH)Z$cS@S8dXBiAk+4U3{o~dzLZkGfX2AVVXw?U8Jzjdn@e?;q;*HO9JTE)sj)|c ztm0!Hl5&754c&rc$J(u(zJeldoDIjj;=4 zxsYzf!JzJo9<&Dn`RQ9l1tX2|r#|qhF5AJ-E{P4gkT60l)rE>0i~xIt&gh0luBqbW zX1G`nIaD4wQTrKAI3|7QGF&b~wK$Fyrn=aw%Fs^QbA|6sUyeH7=<(WU3Ajel(1}yU zBdOCf(bWZf7|!N0=6mswsaeE6*Y{}!MX!J@$hX0jY?R#orqL!+<-|eBP*lvkBXn?zGv)of_nVWeVY8j zHH8<#+d@$>1Wl*feg!QUz5sFN36PMJP2wQn?KlSyGm`XxU9Q@>;Q}q~2~m$*V`l^{ z3;=noWD&4k3LcvTszk1L6f{>9TPXs)8WS9vNM&8J_&C_S?pd5qljD==JHc0r<^n@wgQ<^3rCB$hf0DitHpvmqyF=q6Sean`D%?iN%$ zvjr=j1I1tjSW1V9-Kd6;P$1%)x8L@SmIp+4vzVKjqFS z9}VgmfXtn|JIEHzEHhD8G91_Ug6M$By*EhWfm2drENsWt--VVbgs*e4@MWlyP&K7Z z#jz-D>fEjG1jhz8+s5=C8Q$$AA`}BPgRCI6t3-pZtxtwQNfFo(zk>IjSnA?#U$VIvCA zdhu2yBB7S$A2jOQaD?GuB4Wk3b6ve~X|lQ43x`niAMYN~QM!FXy21Mtj6nQwF6!ul z3mz83%<83^3{B957Jzw-J?|vvnjw6Dy!Mmwf_Oo}9bX}|rSK~=tBd?(Nt0*-K%riIAGB_o{(A>6 zv2o#G2+=IGqrR04pUB`^7r$efdbG_2JL!^$`xP;LxE%!NzbipT)e1Uc zXkMnNR~1eaXRqCt6T^uH0~Q5MBSLh^m4Q@NLkVWS@yEH}OQP0g7lsSWuw$ySdhYaO z{DQKjUu#&dh@X0R##b6&czDP{xTNdzgLZk25|bEqJJg|SCY3=g>-Y8Zt;(W{eg2rh z)!0YWYk{c`duL<*6=Xu)*$U3UslYLLtxXfVVpW zRN}xwiauq8e2=nTm3n&dp4o)eYk8U*R@+8Kf?Vcbc6MVh0`HAv@`r6_@HnmU=V zv;v%225yJ6Mu7Acp$|;YSmq0ZjR8GP`#enj{P6x#2RVX61RI+;9uZVp#s7bly>~p< z`};p0NhBpwvZ9O-60*u(S=l5MvP1S>*(J%|D|^e{itL&3ij2sfWoG}b=Q*7BIp=-e z=kxvj)2*S`>p32e>v3K8>wdpqq#Kc>dQhwyD>ABd5nxZmoQvqJo_n|kFHiKM@;vWL z>Pt>Dv9=#QgF3mW@NGt25%hETQZTmP!DQ~-S--thnb@fI{3|O6_FC33;=q&xcJ1TV zeYqMu(`)jU;{4lC6mK7pjmLtY(OF`)W_%lo>;>mAnkrSD>;;)Fyw}qI@Nkb`1^+T- zzEM9t?EAKE?;tvv2*>n%g1;P-zaPNR{h&di{UwbJv_Xgb0*flDM{qKP=4XgHLo8+K zZLUYWP}pcKNQWjYN1N5KbVRYgY8GNLw$NNa<;9zkU}L61!!iJ-r1|QITM=i}KBna| zwwdp1u{MB_fnk<0o>8NLU*jhDDb4kvgQ6}Z6>;l>x>b)z%UO~#v&@;tWzQ3*a_9Y3lTdZ9 zI{536au;8Vme@iO((QaY)%+(7l-eT%(8wwvC3xqB+E99Nm#asDBErBx!$HPDX!!$I zi@V%4rMv`1=Io2ins+JL91g1YR@<;QUJu-#k-Dai%{?W$xOMZjt^B=UE)4s;7s%2u@j~`U{ht4|`8kn zXGwnyqMyzpqyAQvb?#?hZdn3X^3ezwp_}e(E|FOB7I!2oj3M%c)Cck8Ls9B{*O&TD zC#`$F78xqY&!|t_j0Ju##f`0ADPefdVA}E4?aLLHiBYLV#pm&;4TLr1NG%$$qhrAntVc6`0jLMYiK!*43ps|Ut4BV`~dTdej?>oAHeFxd}_V2?VP zHq<&-2uZ)_vV)E9)K{}w6M*Js)Ls$Yzl%utByUiMmjd&;Y&tw&qBA-Y3wyH2)lU)n z5JuSII-d|w+`Lc-z5i*0tOW(Ag0&3p7PzidZ?(&bkr5ix(Q(DiZ$rswzyt;qfq~Mf zkD#S^DiL-wGH6532u}l5Q3`?wf_IjSmn)DLY2vpa5za)m^_eYKxE)C>t@&nmaCXi@ z{_WaM9j=Z~Llolsyg884os7+Od+M^WKSC$c3E*n1h_^mJWDbc%G~yGvT&=5n8atO- zu}J9J7dV}%i>dBi7xO35g1$b9!ndnk!r&m5m?4|^BI{MB52?LMwyM_tJ>5f2k~#I3 zNO1m`4@;0t_Cle7sA+d2+gmPYbK^_ zr_typc)_75);u@p2BhXv-i_)6g&V%`AMA0#SJhC9>doGd$LyiNO=A2z@?_2 zwxFKOQpyUGj422FkKbA69J$y@Y$cN(<7`t1Qs#b~!PcF9PC#)gd;2Igw;n+MCVezx4tM+4794`UD82ZESbcrpy-!D~<1aj7G2CCBWLyF#d>9^6n2<>(YMjgr*kh^S z_Nmo7%k2Pknw$DEIsI3LJYrm_#~MM%DD4Ed!RP+oD4S{CXwyNc4F&p)Zvm-;e- zi^a;ZA3#pEtglvdYVIMzsE-2S*Q0=NNQGHNbc;4 zF=YYVAUQXBFj(_+{QEyWeB!A9ZH3wf^5`zOL)!pzk-Q*~GJ_&EtJjs3IZ_>^{= zjlZA0$~){&%5{69ggQT@GD$G$AvWtfFO2B<>+bJzMX=9V8X>&JR3MSNd!e|j;t9)>scAwl>` zFw_mEdW_-Si{x-rvVt<54ccMsP``97x9K{U$J9Y>ECt|T@Te}gI!&gM6LuGdxtOTBs)7EeP4U$AYBp-6INwvSmK8nC5EK<(rb@&{uexCzi z_+%8QeV;nwTM)vS%uJ<}GN7^zx>Vo1wM779fQ#oqoAwX}e+)CPqLz8tln}5A2JfIN+#8OifdkH2xflZvhJp!%SzxfyGzS$TIT6>xESnX1nT0=Y}Wl(f|h9 z_dQ3p3#0!uxkw@?h9a?T2$kC70j7=N!1UWx~`CMHn{^1(_cKs*N&!Hn1 z4DKeZNVA&maZOcAIP+Dfzr2B?-Nx_BA4i}sVP)`2bp6m5ubkxQhq%Js7(e&9n#j?9 z?1*vXeF#`=;1zU)gbu~Dfx&y2UlI)vm7bo3-d~Y|fBKTY6|To$%|scv@F}u{h&2pP zK`=nrE2;#G%C}dgdx7R5svu!)veCvbu4%aW#XaPn#l!fyJp&33YR<0OKFQXvu3m-$ zt5+~Ez?rHo>;QRW?_csmE!Pf9QLq;nsttZ7A=df=stdde7%0g>BH~*Pfp`4mA3^#5?`1XI zr)YY`y03oQ#C;05gIeYKt)Ds>hAz@RD$6*$*5JR^ns|ioQ~2QJe~R2(IoqlGRZ!aH zd^06&ur36n`xg*74;RowG<0sNmvzTjw9?&wEvf&vDDd~^01xUZ^=}8rtUjEp(|m{9 zZ^{8Sly*ly_1@xsJFdI>gD0>z5OVVXFYGe2mlx@R7Pi_k{}#Z1U1%N#aBrCO7j^M4{Xf^p4E{s)tcQ`voK)useJOzA&p2@Cw zK4z^adH-;Ye_ew2@nPU#t^K&y<#vQoe-3g<@AFqp!T%s^GX1A@_${gJJb>JyfgWDQ zt&$=CNeYlMfRt!z1PpP}urO$z!xzoa-{`&SPI4(4sgM#r! z?_5xZ&KbTQniuWER)g;^|Ncbzjj(2IF?Y94zxEYpWmms)3LURG4rARc4OB~Bm(nn{ zfw=yDM*AV*zXeYuEb^C7A3-sJ-8AhWhxLe!3iL@t?yl_&9bn&lg0#KDK{5chtm7!^hi_IK+F5;~^o3*A1 zZO2`wfI;r#>tey8_&kXZVQCfJ9Rxv9(2Mab8G;cotYBSTm2bWIx94&c%Kx*&R?q%+ zgaF4h3aX0e7pHKdU`Aae!1Pf_BF=a4tA6JY^7b}p^)@YzRH7z=09{--+!zBbD9O}Y z8NU5>-*zKZu89k%EV_Rp`2u2TVB!x@wF&6TOaPvk$M+D4&WDn7dOF%n=4%uY5xOS2 z&pER9F;A_>ro4W8 zzdp9TiT8VE708H9@wq)L%}WHLC*3vJ0L&3+Cow%mfmK;(52nHjY~1yKU4(!6_}^pp z(`5feX^-aU@+W3*MSL=*zd(UgGgxF8T)YLH-U#q{j0SpA!=l(ptmGGNFDK^X z-UdGf*NvRJ&k_Z_l2n)vZN{Rsi&ew$k`LPE^C69f!!pvu1uHAP#?BkiSs zxcNUn9f}7R0sTMdn2v%J49aLM?lpiMP(qgnPF{wwqw{G5E zgR+qY=uZLVeAt4}+EBKv$K{AX+9TrK88NEBDDSu+M*8+WI}090(uH5J3Zy`qp)PGD zeU6yzhKk49OIEN>Bk4Cs;atzNXzRaxD%#^u_svgE9cc0?Qs!q0y-9!G<1vc{;gCqN zJjD`-2fy7XaE4en9DU#zU%3uIkde2_2P@<*jwxCB+eO|0a{BOwOMY`hI~k;j0)I4PKLP4>|EUoB zkJz*kK>mr!%B`TE3W&XL=83jS>VC`(U)n8bB4>q=>6|+S>O~tcYbbg)_<0X8(lcPaHRdOGrC6! zsQXQtAMk;Xu1R}gMD2_SA5#DMzx~_2&r$q9pjikX5fB1Xp8Z1BEQrWkNr9D@s+b`k zrkcs`j!yHq#%bV}Q}sqNaeQct%Y>_1mb z4~6UQaK2CLdVN@OpK$0IZD-$Q?oI4<0==OZSTaVLOlXbASsBG8v^0JN6D3Z;+F-o@ zW04;RLU%>{U%uGT{m3g68@7$_WZP;;EU4^ z!+~a^gaX3xooNU(PxH>GT6|sTPV|8OOfhnP2S<9>;E9vCLkF|7na&bQ=v*%OshiOy zUQI-Hz)zh9!T<-tW{GhGy|fuv%SPTbLb!>Zu5^ep)A}fkLS)yOEw(`#Z#d_pTf@cJg_1jMG564_e zWd|9$uM*?0wVU6<-@PomKaoDB@-M8gzdT0%Osr-` zR^hW3-FJOhld*&sq2U^(92`o6g)Ey9|+?GwOuHy&Q@JQHhJrVyS!NQAhTiYVQh;lFJIXYGTZMz^( z1szYd*eDcH%cx408%oSl3#7)}-WTk&fjajbSrCO0z=UXexC2kYTwM?Fyu?Pteu0!R z*bk}MxUJ_^bDc)JcYracYY74zTGms*^l|6J7FTsZ;{V{nAqV)IBqE*ccF@ae`Q;(B zGS8-@>rkKYU^QP9_z8Rc^l0E{$E)dFqP|duus|wb9<^*1bTj(PY-d|X%9U!@h&&yc z>O$8qm2C!zHlM(QME}KIV-Ie;(^nY8Nod7q3R?~iRhLmR1*wfMZ95lTt`&N5_A<2h zwe;xQd|`?jB%@h4j>HsZct6gttw|`osWP`Y5qJMFaVv*^Q(|TJXXk2E0&E$Ds6-ntNRF4bs5`W#wGMU zN4@+rsbxFM{Wp9d8EvCvm2Wo56MI7*wx(%w7JIN^djs{D13Bm{P8P?aLo;0$TICgG zBa%qV8yuv~kPr*W<5h)BMY;vCnNrzA!NChcxf;E|bumJmq?R7+>5TaNbQ5nXCg|FG zJ0xmZ^tvw%Gy~Islot@CwB~ROY2G$}gV1oEffLx`X!Tc%8!&hA8U2sjm>8=VXkW=# zeOXhH#n|Pi{h9*3g2Lp*nM)r2p}ovSPxCyU?(%G(BIHjAvZqJIw_59N6}(>^#d0`o zY`dl8DACXPGnbF2~m3tuULO+xN5(R9JxH?_Srv?L2;^cQ0z4+6?`d`0& z%HocQ-V}4x`oXb*?uP`}DSyYm6$gD*(XsHfYj>V~c>;b{7m4Th*19;L+b8jx%&DJ&6K0v%1?df$HF9Wj2;%kGxUg3&TB>;c(zjyCc~ z8Wif#Wki6l*X-)KTs3B6ysVbfQVPl97ZhOXZ`)fIrE4vapsIbWgkHM$PY_2(Twym- zoLvLG0})qas-RQEym3?cFYw$LL~s!3yf9CL@;A)40ys(o4I~!uqM(^I_AsJ=;kH>MdKy{e z&L2nptq)E3#S=+K*K`n_`a7(_-JlDN0gyJ z0d&N$2qK`FfvD0jFuW`=Rq7P~5|;n?fTNe*jvt5z#07`M`wEdwE)ZKf0DdJWc7#AQ z4IPmCbC|VztCDL#){FvhON>nATom*aSuOgcyu_J$q^khk3cJNBE`B3x_!FT1Q(x7W zoe=&8w2{x>j_DUAg_M}r5(+rXthI#19s$KISIZ@Iw1b@LY=w-(4HwZTH|n!o;b0eqysw|Y1MYi%0( z;pbn|HFciCxulvKI=TS``)HI9*9{&d;!f!zHDb{@Y;>6UU;q|bp0Xt1a4=o?$(;Ke z?GFeNV;_Y>fu6wvT{I^tJ(yQ@$D&dl(Bgg@I8+B*2wS5KHEe)INjVvmUOftomJ3bs z9Ahd8H-xwh}79)`IKRs5MH1B{E$A0Fw@|^u?RLbR~W5ka&*x z^Y(iou&+JM8E4*ihq%;gt~^G%-l?1hHJ>&S_~G8WjGn@F|HXn zf^`=d=F0s7JnLRT`&05Zj~EtyOJ?1xB>DZom!&cqhA&oX?-eAyzQOz^LPVNo1GqDwptY?9|S?s7dWF&*J%GnLg+ zfA3F~PQqi89m!*B+LuU`8C;(jbAj~&{u@nT3fCyGL(UGQqS0*dx@&FY6l$t^!Mt1D zBEF#PG7%)n#G>08!h%4saQ3d6(1-Jtb8+KHBXYXt`*Ld%&4{hInlqw3f0e~LiBI=7 zYXWAy-eP=IQY0MOSq6bt!;8gQO1lKQoFv!-R&j`}rnNn|5O;uvd~x0ts&3EGJOm;$ zgw-a_X2=mjG7AnI8v9j(a%5+tw%+3P&soe8-bAI#)faa`5WnIl4K>wj{yxuT>QTre z@{MDovf%HPYeAb7g@6OGS&EuGogb#AIiG*4D`RrS7N43{1LbVl&fsgY*Thrw{tj>f z(ct6Ta8LQB>0E`FmDwzLLA^`v9CtwR`4PU(8gsL#b9l|CFp~UkUwAtGf}s7pcltp6_j?2`R!GaqPy* z29@|b_nSj90(H{P(6wrPo_jwVJJuC>yWcqExg6JNvu1a>Tz}x>oF^mZwo2cKBttsP z!?a6*mC5nDNzUp?trxC~M&MIRc<$^$$95X1Yj+S`%@^XBi6*G`D^~e8xq`EF0F3_V z>4O|1@XX?SqNsjYS0oP++`o}&oDKdniRIMtq2FYCG8Aanx`obP>2L-*p_)mo*2-hk zFpImdsVZXmTx&8A6Nm*^pr(jCiI~BlM)afHS6sQ1H#kyBBgwRp3EkcCQv>t?J-Mvx zVMH5{H(GH8IfyvJ!CwwWUseW-@J=@qwCS{RJcC99C9^hZwemb8fTs2yqfDmf`L%6U zM{=`7#i{GmcmW(q=8`k_Caj-^VyVeQl@41-wlj*W?jd6mo&iWRDT?lU+w%KE-3PyK zDfdO8I)=|u{aeIyqgsss;xg;rp_pMdMBs*8+hy@BY+a}0YT~`On@U2<3j0G#@`&HN z#(UW5QN3o)Tr7Hbgs|UV(m_CBgcSNZaizBwD)m4d z9eIzY!EG?Tm^v(v*W;pPbmI zyB$NpYveyh2{dP6=M&cXf(1jycR z^v*z!MbOje``wOB+!fP)l6O5bcLdHMRvJk&4@E!LcgXv`=F|k9%!q-fI~~T_!h5t=xjgt~9GUsvO-RuJ29(jwcNgXq6Yrd6K=+O@ zcQ2GUe&N>$(!mjU_K8eJ--m5aBp-9H65#I-R(d!{0u~?n7_Z715#}qRw=PvkqlI@bMkI^wh#hagky%gxMULy`t!n`Zx4{0O&?Mvy0yO{SIF-K`BJ% zvYoaZGuX4aUErmC#}eSKM)leT8>3xFe7%NR8Rcf$7&`mxE5E(Kk%HTHY6|64U;(a9 z`kF-g0D#T1Bx#(5Ktc_xoCAzx+EJ6L69D4U^10OLq|Wj7{`ACQ&Z$dAV%>WK$10Af zHo8&5T7ddRHC~O{l7VR>Qpf+iBounuGQP;rjRtypNAi_4e(v-y$})PvKXC7G>r z9;4FKP4_*sI=5_NW1q>%q^2+)a{)}+aXNdyfkX2xAWmaB(>JvM)#($DzxAQ;MCymn z)^C~z6}Kdd@<;5d1&ufEqbzAm-K^aBF|q8tA|04z1fA0;?wxFVa_0D5t`ec;y-Ic2 zDuXn*klM@tCF{m{r4JX*?rZDeuy5$Q2(*QvPQN;?qA1RRrx)4ac6c3UiHO2)p8+RA zh?^_uuwDnzjD}wfu~RG_iy<0ngPuY=VtxfDP&*_u&y+GYY8vpmMys&*k(z5(!~NUNE5Y47UYu70ZE#`sFbg)t!FW=i^=kz(L6h zj0+B67Fxchxscr8n-Kun{v-5z`cg5RJ^1%%h+EkKdExHUn5Y02q?lFvdS@Z1o35M3 zlF`OIf3XpesuFX>*ctzggQd8|+mJfc9FZL)^-*Hv_Ec;!3h-|M;!W^1#t46l`({%Y zoL+qEGpEVz>hPV1C~d!h|02!2RfWw|LZ;>P+435r$7i^n4gf82f)jtarMTBdjFiu% z${un=p`~g;rMxd)+nwUvxv{*?JE}4nHnXG>>5pLk)7ZTc*w$egFCSA`JZ_7%{PFEW z;y9FKv-rlC)#OR$y+m+0H_!*EK3qEo+>9bXaEoyLK7@uB3GKsSHJR`=nGD0?Cfe?Z ztK-S4wRoKe)HpuB*G!YD=$%_@DI!JeeuIWWR|xdEvzEc4zXe}!NKo1{a&Ru_3FC3h zr4l;fu6KKV7U#Fac8(??c?Z}O?O>kGNck>vS_U|UpuJ%mQAl%gqHgE0n*HXWMHzX2 z{e=Vo8vgjbq4Y`?xG9&38o)g={fs$4>C%98bpV$=vONSQi2n;Xog7k!n5;2X%qDe5 z;2ZzO@8qI#D2hE?6i}opfY5{eW^NG%vuM4u#a^3gyWC@cdjM2@D-83IpqT!DVeyQ$0w13NGMpO@SZ;EYg5zy0V7Q66V+r3*HZerJp!_MO;I z^oY$mV#h7J_ViOE{@vCqJ<-uU)>=LoZ;kYQG#g)2-7fkH?xiEhvM#y-kJ+@Jwk9A7nYQvfEzP2LMqtX+WG(?g#Xr z1O@vjP-{TD%0^GK5pTXB@b2-KbMNub*Fuvv6=GzfUQbzuT*dNR=(I%viK)xB8ehA$ zp#KV((Z6iFV$~W#Hx2Vka9_N$&SN~M2UHv2#32Y9^N8pk7Npr;uo&sx+JA{4zk+!n zRiIlg23k8x{D!U=p8lsYT9!t{1#!A!A=lD6bm1{@E(or4&VMJ2^YgO}5ciNAb^!HC z+3+4y?ViPTO7)xOsTS9P!~TOzZRX)AhJHObil>1;iBp^f1as<%!s}6-tV5*peBkRI z*qaqz5208JFzOsbO}lZgTVjm}kxHuDFz-J41b{2QmQA1(M0no=EP46;%L)6;(44*z zKk1LCl@;?e@2$U8Nv{AxxMkxN{jweL6PZcz88tRlx$*dAFM}WN0c;oM#_@T(WY;eC znvO;v7$FtL7%%x2VRkT|*5sioQ9@JZ`7ICA#u2UF?PKQ!ySn`0#4X()NRIATti$r1 z6T{*RTHY;!()JjOX;s?aw+Y1-!Sf0SeOh-t(T2!PK8a^6skqm9+?)5qDR_Q6e_@k? z57YCm_KSwoaH397=0iV^!7uYnDHJM&74(Ve!e5}aG?f868$0hp{0a%p0|YigpbAnV z#AymCLSQWspj6pp0^IOFAgollx`JO{{pKe{_YbQToR^Y~G$im~7VPcZahRGV!a$d6 z)D`KAU{#?N2o*GG8<5kjg-(Hi0lDCDSCeiX|NjgXHwplKBd_BkMaGtRTL@;-ny*= zux?&ljhJmG2zY33zA61M-=C$y6g1D=B!DdxE93Ul|7(S{aUl#ulMRPvRV$)kV)6L} zjfoB*5I-yD;rs9q zu*PIqrh!!an*Uo=ByFz!oujsEb0J36awH&mK5~RC2=S0V75m1|OpM$h-yUtCj;2C0 zj3qoxI1fJqNwp>Tx4mGcHDsziqYqkRxg07@e8W7qVIE;UK_lHt(l`8N;4c|$Wk18} z&>W?u9e_@3V{v#|q8EqZDa0(i4U`RiVPc+_8w@p>D&K@H)Cmph2an9Ebpdz9ACTFf z@}~Qcw5E&coI4>mcdi;M>FEKKNzbT1>p_Ou5hP#7xUT)8@i z-A57N}?)&#zdDrM%7~#zFd)xp(_{$y+S)MtjX^9j1A*&wPbug*O7OR;TrUb z1JWBDAk#V32_ zndl>Fs;AD-no;ktcA=Guz%$3f#)<7rMB2nkA-(6SFE89>rf17E=IuuXSd^65^~XI* zQrEalpUJ>74X~{zZh4+2+*O2rVD|iZ-GhkEk;|;eHnH`RL^`UA+^W3db)h@D|$c zR@h>av{WT$??RLz4&nP(-0b75_r;F$S_(X`6U&k6C=!xmg1!S>bEb$?iLV9# zbLX%0gH#_P@f}9#GkY)*zfZaT9$}FviQ%k?oFL!PK%L})H4w^2gPU9w0C&`rqUjh> zK^1RFX_c&PN2~0WKdXe=>~FpYtq4PbkMWFdz82BdyC)ZY|l zbx#);io{zy?PICUEP+QJkQc$+?e)a}{#5bz7=+IJdaFnTF)t}jEOeX29= z-q#9DMXz5kaF}j^15UxlH6Qwm4BJ^46=V|a3%c%BjR;kt$i~mg!+Ri|cT}Qvo;mG7 z|e62Np)}{iv$retAioSTUh~qKD2n- zn^U(y6*b1sMKvW$6`*jT9`P=22Nl8F6p=+%Xh$*V2j)fqs}<3%I{B#ll$oMut37ic zb30e_6HP2r=w{u{A~^CDuHI|S6aSI6=b=C}tL~{OuB7>)Q%3|}pBG%;m48asT4o96 zB2cGljkL%k6M=WrR?`>v8zM{UdLC4z8H;?Pt>!91#zCk%%%vnU+K)U4T{#=Xvl>T4 z*sdtkam67`X+bu?aPQa8Tpj$}ovCMwqOByG3n=D3eaM@UL;`1jaA9@!B6sS}(9bg> z9_njIftni(%v{ywOehBnJAF8)hz81EUurFARqDS2{XHu#Gy2j&@MKjX!Oo(tun}cL z@GvTTP3YhmT&YkFab2r;!;Q*tY-*82=|W5RMM4@b#`}9{kU}3^EZ{$0f_E|A2lKNo zO-QM;mLD8b(3-Q57>dlh&${u21A3dMFcjHU?@KCG;}S~~a7a}IHvm%_rENtwu2XgqOZn?OgOs7Lzqpz@3p z$qU z8eY-G)New}xwcOKqvGeNAhdtwc1BIYY04iyPtA*szz<+l)lUq)7x2C2#L7<+W=Y*y z*Qji;v5=RtZI)K#iczI>PkBk7nV)QN3EzAh`wq1Yl2Yp6UjA9Oij9xWEustd_kLp3 z-u=W>w}QLd&rM;6eqC+5N{3PgHwoFV&`5gtQOzr!Q{j|UXt+XRCs1H22N-Q7xr}za zhUG(f6n#g)HCLl!5@QfX8r8*2)?_S{LJgkFYeOz_N(Aw+=BD|XcE(5)XKGrJZQ$I9 z8NS9)Poe&UEZ9=4JA*`nn zCqA~HJ*m%qyawe(>kc^XEAtqnD#grMb@cH)T?93QUg{#ZHpTJJyk|ihW-a{LqGgEJ zJcWS(H`LWtElehl1jVtpsFoEzD-lY^CSoXpw~pXdXQ*G5rzQ*%F*0}3uf5M~ameZ# zTFaC#2AGuC~q>Nmh(B^>(5t@F&xWK<_F~5-9}L#7Uh>51#`MHP;c%qP#D)X zuB**w;HNJaWpNz#YoJmpeWca}&SlAGZswhD-`mJ@-8d_D^<{TWk;JnH{ap2O)I}<< zK1Sp5pFqR-r(e_wyI~{|2k#=&S;9sBbr;Xk@aGi|qv_|pWAdsuqYPwCp*LL3{o2$Z zBl`Ky=;b*jeSw;mEpnMaGuG(0Mn7bQGL$#!kbFJx8xC_7-ZPHrGC&l2hdEY9_Il=p zyHd6cvQxJ(-*C_0SQE2Gz4UgtD!6iQRy6CL*N=7X_XA?pGab?TJ3Zg>Ov?<}$0UN( zBIQ@i4134EX6fw6Y{DqI;K8#Bk8Gn&!RS^fWYd0oQ$bet$sMbX_I9?%pLYW{yHiG) zi(zJmGX7*o zkA6a|u9b1#-#Pv~ZrY2iaVcY3-2MF2O3Jz9U7pv(GU}aPC(yAd+~614d%?!=!ghq} z&V1XBdzAHfUb?iQL3k|%JW`}+q@yMYyNi6NHX;DQ6e5Sx#x!l5L~&tpF$8HY4mYf; z81`yPTL7myiD$s5H}*xbc5(?&iF+}=IemrTGzB_Xw#0^Ia`bMJEC-)`|{FkzfuY5_lrRpB?4)vc^>oj=Z^Y zfnWo8HXJLxBooBlg;Ar!y?EbX$k)XmpFfj@S*Ja9?2Yaj=n#xOWGo$?*s!L}Bg@jk zeL9b;F%~`PP^e~`#|u7M9m_2a*Ec=R^WJJ2ie3r$h(>G5Q@3?kKXfQ?n(U&)Q@~ZYYQgqaAUG)48EI6mhi~yNZlJOiH|j8|FQ<&Kt6_e+!q~ zNL;#lVPRq4Cd0D-HM#3*aC|e24|lwqV`^n|s%f2f-2Vz|RRF3kT9d)y&;-uUg@QZ) zIL)3%&JFv^4sU}^#mw2uYA2N7e&DI(Q*ox;)+U1()7 zJqkbhIA31TZK$x^SE-L25DJXqEw<^O&GWOEqeK=y@?Q2S`8V1RWM^Wq8cO+L)NXiY z0R4ocPPhDL3FjU5tnJY!y$KQCH=G~c;)u2iSFFBoY_^H(xHv9#FSVdR?qKY2*ERKh zT5T#vW6|dnLiIIUf905>)1yviSWKWUU&L~PYHW8z4!$2)Il(Y}y0}Tgm%Ecv$UA~^ z7Wawso9emc#)_}c6H+Y)NIq?*qE&&HNkr{u`f%ci#f-VvO1Sk0Fz9H9K|c^tH>-)3p4jAaHXpOPK3H=OO~xaIKqK5b<5RJCCfsinWlFIC&U{YH+O z3l=IB^#uoa>y3;;0|TEN{DRD>4MqljD}69x^3e9pa_*e81ug-ALkkY`LYKK^s<971 z$i2=v3bR|IG@Lh79Tmh}(Ix|FyGZ^itpE0RNfHT3Ngowob>|0rGa)4NFTcP9;d*g# zand+jPD$EK{)=FTF=5xnoV?^@sO&vPnaj1$WcHA zm?mVQUYX)$3Vi8#CwBU7X|4SNX5!(mCcbvPT8A!Wj)%B?MGnaawrQ2A0-__$>w!Y& z<_fZ8hl{SK)rg^Ybd90jjT0|*RM|an!v5kBdH3tp$HraHF8p=^`ih_}*zX_E-4c18!AcV1Q@dCkY{k*-AK*2Lzbspw`f@7kh{3J$V4eHcIM5HkGFgd zMMCa7W&Gl(UR1zl7TcnUg+`UHTiI|MQj7o6yZa$e$Sy=RobHm z8T5NHHZJe_@wWZ_*PzDHCBT*BCbB?d7K$1tE?+=)jF=; z!4Dt45MZg>RV$f22k)5=(<@H(k=0ycGBN>0rKOV2&ebzZ2!kR9IDpe#FpP6kv7+dl zY$-oSpLMJg1IZ4iVe!XgW!4Q;U;P=qd6c!v1Va4RnIuu{*Kw+M**uc~!K0$rOhCXL z)ond94C3=&v2SX>=bUtANWxSTTUe+Wm#QY0S5WY9uAJc<3(Us6hLC}3mh&u(9=IF0 z(xR;U$UJg3d*crjT@H80+CX|RVLdW9<_JsbmGZ-4diVKr?fNn1-ZOanqjtb97K&pq z`(HUB5>kj zPTUW`qQgl{%LLedsL08UZ?6I^f{9qY_mUB3?-r>8s~`h~5f+5$-PoBn;wX^f<8;JZ zbEuAG22{w&J%}cl>ntU%>g7+WhH(NyxwQulU5_EG_NPi@)J`)~Whu!NXQuTpj!W+I zj2#qZPMJ56>7TO&KmeCC4(!oVwGC|rv~t!B#^g}8or&HrczAep`_kn!M{a^^^)TM1 ziNib9yz3($)au{uI6C%jl;*wCv~}1wT)W&^7Fi_s7ZFcD zqW1agvXrWfhqm&X%Iku(PS7eb^6m7zsXCXDogD%M!F8feDM`uT>FH_HN@Sk8*>;DS zi%v~0A!cz?DcsU+OsX6gw&6|i9(>t3h#H^aJ1pPZLDk;z*}*|G zqCiHhukyHzjDrn%UMOO&*M?%hn-MCSGfncW_+a$R%C^mOKo{G9kFl`50KlsB`@+I| zt~CH)Pr}rJjlG9636?$6>&4$6Z4_2j4SSy6F9xAsm*%*NYtyV6qecZ=a%^(3MVhm7 zC5ea28-wSH(Yd9{iA6r*hnuCfewvobJYd=1QJmQEn6tN{(Q#)qzXnuJcM!S46p$f) zkmjP+!LaA6$%Yccx5clA;7o8`Nl7Wr9{66$2xX!D7ANNMs{Y^Gio*NU;s>cXjaTDd zdPcv;%Z75d>Mu{r(4&yo5$zJ#`<&+L#Khjb-p(ICW46)$e5Luq%%0R$_cHt+ zIoC3R59_ZUP=M`?W*+IN%dSZ|kPxaycY(vu3PPd_`L(5^4sD)zONp$wj#IaUj=2E3 zU@$5S$wn=CeWdGl+heIvv0cX;0;5x+xKF?frhBiyu#4B2-au^$J}FF+sk~vYAno}y zJ4*e!w6rubmc}Zw4Til4pR{RdX_+?0#l^i1ZOSfl5fKqhO0lubV23A$h8eS}OC~_A zNlFyi=XhT`{WZ_jYLZ00KQlp1S^?MjD=6jZRWz(aXYzp6WrMUg?zs-gj)63Ee7i8< zK~qM@obgO)?S9Tf+N{B>*J`e50!p|8Fx4woV>*el1?XT7`_;+}{VpE3r1RE?ojfac zJ9c0IJqbo9vldKa8_W|~8A<1n&a=)>$F<+{u|9Ps*^WOV%?5sC|4ttccQM@<>Cy5j9%Wt2w4X}jE-7v3$ z`&f_eB{ed{3_JwrTwZMTK<#aT@H0ag)cYjWo3^*NcYm`Od`bv@75X$}pDae#QdSPz z{tn{AsW+~BI^n$8nZf1dl3_Z&;co=+ovs%evA=ubr$#Vhyzn1;sZU00;&nNq3UQ3@|d#>l3sC08tPBK~W& z?h3mk*6kS?8552&3lYHBYO}3bBdguCwzjr_H{noo2gXxI0?l-$OD(6@C0Ha&4l(q1 z8kvLHFv_SeM#LrR`A^)jNnN(5!c#vkK*2~w?6hv;;Ox8>Jr)9PVAYcR3NvYkkEJCf z&e$;;6UsaOQqIa5Vt8V7I^Jp3&wyHLm($qTxO(RRPNS*avBMhF7=#E%i)Konh}z!n zS!i%n)r|JiD1rKb1?^GJ)A_I2Ptx|MBlatL*Q~ajv&Vbkq_wN)>t1-^vKs6rL;AmR zng3)p6nK^B%Hz~=4_}S2>**ckJU_0wKUe9pqK+|?7UZj&tM^>Y|3DpZoTd1x#=pJ! z>iJTm`#p55hH!o{F&{)T=5o@FIH31QG-eNtOw0}F)I68vR5O%gU>b_vOrdOEaoMU& z{Wu01Nz+VOqZHm8%-KA{t7)+|Q8SVUA0vq!`dVW^CUe;pB5aZK?$@!+`2#ot!;s8^ zV50E!xk?;;H8ioYA8|HL;M3Ux{N^FuGb-xlULoP(jt4H#CX92-3=p{I#6W?GK2QK^ z*q(*_S4ZCj54kpWlMGfR-65PY;^_FL%i3owsd+QvM#nzZg#0`cg36+JFv2aGBY#x z&N+fIp5|7zbZvN1R7SPo>=$z3&811;iLLIC_r6RxfA$@L|nqJ*Rvt_`HR=6FaN*Q3&_}!8a}=w9SeD zhZv5{Z$)m(=zT`^#25=1hPPxroRBIH40;CkH0zyeav+y}`SK;YveV+SI`L%ROP5WF z5QpsA=c$;>XK-+&RS<~24~259SYl$!I6aFhQcLS+BPxSd-EK}yhC4@A#Wxs7W+dnW z8#2>d9F!K_y4*FP)WsF=s-u#;>DcSGA@*SuC#&tR!c>iL@aRBgwWd25sy+vrVPhTn zXrHU4@kV!Cl(I~Jftm>nHxG)BH#_*!-Aw`Q)=ejAp+I8qBB&NRfuKeM-w@lo^Q0t`+ZLdKBlYbF~}^``~pAWoHoi;pbW`&4jHPfl$z2k z^U?i?Z}hokR&&`e&6aIc%;iR9T5INbW@u?>Ty>SsaF?q_5A&mG})szaw+I=n8H0r0 z2WlI)g5bc4rlO_YB}?)a;4m4YLMBtbQFy*O)RT~diHo?tzsOfxk{9wxWOH5tNu6G9 zHR7S>>FDf{TeH6pHU;aM=nK2nWIoDz{jE9f=tny&rrWci0b3dh5KcxTo@#B#JBI2rJA;gIoWWj9E0Kj~=6 zqcMMzr}~ebwR}eP>S}hOtnRRenFA!Chv>vaNKV@a1uqNA~j}~n=GrF3myikF0ZO8F5XLLwIe_wc&ex_NZI#u zW8vO~3h}czM~CT0nkR?P`o4UbZM7%Mi7^b_DH3fx&8;%vFY`N8z)#V7Dw~Wm+pugy zfVnl2i1%oL{_Dem3**N^_hb{Qn~=rOR;+V~dyI2)u5j;<2tYAw1p} z?dXz)z1=nz51`YoJCOY_;CyQ_)X}q~j~Tdye_zk;L>KZ8@ubow2bf4%K7L~Rx6Sb9 zRK$U@dvuhkqoV_%?yNbztPRw2ajB7tz9YT*|B?6J@l^N!|9Etiks?Kul~Tw&w&IlR zQFdgNO-7NuPN|fUMD{3~?2%-pLiQ{qC0hzvnf)Fw&e8RLU)TG}=l1>ccl~vhob!4; zpO0~Wto!4~57(^(OJxGAkrN}w6 zbD#D#f-o+;z@|^?7n`EA51)W~j>+Pok9>Ba0S&cUR>w#Z&LH3)`BfcV<*y z#R)hCH=*YD)_{?D`iy8fVYEJ%%FJ3D z1?uS;8K*v~$mMhAmC|6RNDq1b>>u{ALG7**qiWi;G{r229{Sf@S;(p)zCWcY+jLW> zNGX~I2|*|furLCs>NwfQx%9Q-*Xkq}M)LNq2M7!7dxD*Zm|%Qu2j!oh3VBi)RL_SG zNzb2a!zQz%CMG66{PZeGRH!NzL+-r9PZe}6e}Rn7WEc7sQzW>b?`<-!A?NxnfMy_o zuYXRw?!19nAsto9gh4x5Ny(e;kb(FCUBUGL`Wmu(uG!lk6%goL0U&cn{_^D~F3_Uv z$p-_9KW`ZgZ^5W!5u3~A7ZqvN)z>FId2$9yH77@M*)4`gM(PVJ+K+H^H;3}Jun`jb zuwsPK{M>;!S;YR_cbtula@OK$mS;~U8eJ3(AGxN-v8SD#A1*KIm!hF85v2nP;R-hMgG3S3On%Z6l1e-;u$z+c(uT+Qb;LQx%!8dnX!_+j{C4d$!otKwKY9{?a+S(-LIKYeXEqKFKN-SdJcJ}h?PH3j4f5TN z7k2FEUib^WuW!sLiKPqyWw*7p9nR3oo=5QsUFl+i4lD()Im zKjeqENB^R&-Wi0apb>2E-XkI+-B+Y_*PUi(XuLgYg$z`oZqv*Vl9j?CK8cHp?rXV> zF@yg;x9@cF+0WBLFXRxv>D{(rzY{%Wyr*(KIs=96qR83FXJu)rNMR2=1fJU?=$a1w zpYHc;iTD1tr|IdzZEdP)Sy}VDB7#g4vl#~bObEWxdd5on4QZI#;6%Gci-03KZIxe8 zaK2Eid6~paR2uqMaj%XsC$tHb-ebM*P{1}q56V%tpomA0SR*1LhLgm6OifHEYbVGA zC-LA8Z~6bh*!mJ<=jRv+z-6dHndyjsK)Jyqf) zHrt_8joo~TK`<}=y@i*IiD!?*3x$C%W`QiRs+QJ>2D2K+<>ch#du!>~hLDhb%^bYE zl(2Mh^Zftk|6P{GNJc{8S{33!92AR)(vU+-NEkr7Evt)*3oqK1^#pVZjSSPQX{f6^97OwE;ce~zJip&RsA8f}ltqh)UA*V# zmz0_++t%LxiicepiSDYDADJ$%N&1~+Xz3B6;H2-hoYZEodpmfXJ29 zg7wG|*=yJGx{05!yK{qF7YV|{$%ivFy$(me9m1n?VidLAz@kT01oorIrb)AtstnlU zl(j2FnGzE6ZcO>f$<-)c2nIWj21P_gDO_(jP`*1HN!x9H%s>C1cRrz}Y<_yWyu5-! zQb|dVJ>i+iCY|<+?qDtaC+E!O@zNFPeXx0ws%e)sxiB4GIP4TFmtJ<^QRtiksAo$V z#@;@kpP%1ZAJXHonBkmDua2P6?WGQ3O;4YmAdJve{Npph0nVO|FDuiV)i1M&)z#FL zmd)SusB}Mp7oiXOIoIaxd?q?98l5)OGWRN`2ucs1&E7n~e>Nr>(aiQ+W<8f^aYyV&$e#<|25poOYB9eD;d9JRm zE~}>Y&@58D&Hv4QGv)rhpJ@y1kg!ZEUg0+xP_~OV=89B`O7BbzoY9SRd0;pdYD-EJuoMl^-D@8doT|8oEEM~a#M)I9Wvj}J;tSy`}|U>`O0 z-ZuZm3@piQcED!pyn(pF9M+i{o~=2fxfdT`45Nt6s(XX^m}yWe@GPVeOFckTegWHX z;|8HU3o8yiI9xjGfXCwh@1)H!_TJoX*21l=?DO>^U2K@sNhSjIO^PSnpdd= z7iTlxCWJRH6sGVOmv)ResIVCBF@D$4;S7%=H#j^jZ)usn*hy*}$Z)Dh{q0|%@xR;@ z#)0f_xRvTju&F~fXM&S{C8?QZybDY;G{t!{;MEp;06|ldes9(BwgHi>?U1?gKIPvH z$O;Au#(9I%9xIYxHZ?Y;s2x0v>wm(l=WjLt%l|R$AQBZ7P0Y+x8vpt=3E<&}zedBC z&tut`DK3C?8nOg#{?-KHJM>CGKtN#Su2E=La(Hr#LnPfJcjh+LlUck#<#K(V-TC6e z!e!6gqZFy4$xWJ);UXd;$)cv3n$*~at`bx@IsRz>&3qFO{Z^}!M)Ax7XCet;SyeS6 z5?wd==y9rPr&l><_LIb{q}(`q)549i8-D7lO4AdvlV%Dgij0a%di?n148&SrRq54i zAFaFF{k0wA;b10%!^01}Z8y5|?~;T2>E|by9Ogz~IIRZ`$rEE^V_bo^vnYKJ!Hqn~ zr}jtTPW-SzWq{dFdNQ*<7fIRkB86A z&9(LPjJtdT?%C3pq{h_y_Jw1T0mai`6|>m?!3-Jd^vd9S0sel^Ib{ElZQ+iGCf9lx zG=|XnJQT8sEv_xTE=4SZ!DUuoociu2EuP*b(XY6Z#y1NuX;?#|-k*jv6VF9<6FfuR zNDE9GX=-c??dVXS$=F|u?A=2a_hvqB<=)Db62A_@xVgO;wI?mPT0k9|dj}8YPdiB8xwRN?rmX@h%ZTVbL>8D6AnEE2nu(@|yn5 za9Ubg6rW_y@Ok_{-IRw-0mi=838n?1TrJLdOXc=;ot`c;8cLBD~V@#El0w%h0C4qIq*t1YaM<;S!+Pqj+DCZAg}>y!^e9(D3llcT=M@|NbQSp9UF9MlLQc@>%!r zzpbyAB*&)GfLWSTsKuW)lRqIsiPI1x_C7i{H@EcxnySgc=fUr!Obe*)?cq1A{Rn93 zh&c+GeiOjt`klPj)uGQJI5N`GvydLL-w-xuV&(F{7bdN~pQE@ZeM-o=0S?+^PAJPNU_VdkYXla?<2rq{R z+woV~VsUbzrQM34Up@988J@CT{D~1UF`RGSyirwGKLTdss@ZFw1Oxci*=%Ms?%uIe zBweo}AnU>hTaU%#qU$0&SQEEzo~{Et^y8@M-6KSQafR9$|zIJ;e1=d#R|FuTn>E!s*Sg{)-O&{ySp5 zzr}@PK0&6Vv8610AU4v~qCBLyqww7gC()X#?2iXg6vW33Z=nnKm3*(2k*wszp3g8s zVpGkx9e4}+Ow-pB?@s@A6n}jlCS5{$dipl#rU!$ps;w<2i`Jcrt`UH5%xZ1J*L+^b zW8~!Sj@qyE%+h*#JgUgHUQaO?h_x=f;1xbrEyaj;CYU1#Qh%*+H-PsRxE`~orxiJH zc_Z^%imRcUDZYP!3}BvLKfxSlZiL}VcnoT_?01!i6thT=q0S(-yPd)m`aIGC*KJYzU8#6HEdG5F|MY*pqFBQ-kt{NS zAxH!L89|8BRfJ@A9QuG*BR7u-dYn!Ob6|wqwa>Y3*7TQ@K*TFrYZ~r;TaO2jkP;ys zSsDKTDWUbq=_w{zvqn))?!oKVufusS9mby={vH1LU@*3*r$MF~8lfSN9?4(7UZ^Dm znY1U(2BWvTeE8j18r*s*oWU41&ZSy!Sn?A>%93&T&&D)B#@0U~$r%4_6qAT3uaXjb ztj+V7LNIX5w_A@%&2|S8wnP!q)pkIj4xxF8#Nj*#O4pwgS z=jZ1q6&GIs=X>PTsZ;YR;g?<`qS2Lf$q=tkkKrL$94;N8#+w+5ba2t<-+Y<*c|P4J zJqVw1{J%&J6GgQ5ZGPykN-8bYWoKtkOiOz^Q;k#hC=(h4i|+~yNzjF`V2qGm50_iU zD3XCKQr2K#Y=K(9m`~)voA#G=H!&XZ*(dO1vbv#bj1Rw;LKDho7YG;L0^QF^4ZhWY z^XrIjc;}k7wl<1y*^8DIU)lNR+r;1XYj32ds-Un?4qCC*H8mkjzzPHuvcWkO?l%#~ zozv$c#I%1GA?jCrRyf9p*JnEwZ@Xl_VwHD=WunPzJQan7oih+EoD1ttOiGeN(xBQ{ z=)8{z4n7RI3>87Q+gr&v!lRun+Q!^is~_jgK;PfstnV?5ZwM=+gtXH^fj)e{5%Jl0 zI2B^u5FYZhXPvxiYHCs$#+FQKkUjRpn1)jaw*UL<<9>?KB}4r+)YaE@<8@geA~q|r zZJ@;&E2bhu|KGXt9mE?&BQR0Zg+Cu_)~M4%0t~CTx(dmOvmD0b*}v1B__O+(z^?xQ zyGEIFCK5U-RCRRN;Xd`Z%gM_J7ZmW%OvuQ|kuW!|X(ty8bM!Y9Uod6G2VA>BiXA8( z{zi;zkPPmQTlyaNLI`o}OOlJx(TCLN(U3F7Brhwx!N0=4f-`6~&?iA+HF1_FQI27u zp^M}erem>-!y>`VtA}-%|QJh2whxPKELXe;LCC z+jOIGuDg&!RXu;62}~J(DDPb8hAfmZklAj6yuW&be|SXV`+rj>`!6_kI(U6UotlGR zH)_N6G^(OiaMxoe%k<<`gw3udW}y;%t^#!!p5F8|r(E`qL~H}DN`mwSfEpP?2737Y z_ed~}N=gD6d~-V|>0TpnJ1zKk$J8Gl)mI!F7`WTQ%Ie|Lk}EWOA4#P3P&g3EQWSf- z{pEeg531WGZKhC(9k@SBuU$p_SPIoBJm}r4tvff*^)6zWu4)k8f2&WQpOo|ll5v}* zkXRcr$eYhC(7lHE9L2Xi${vOPy-h^=k8)4J35DkblcdW=(cngm=p%@5zvpwVGGK+J zzhNqq(QGr4JqeGHFmib#MSlZeTK<1HGqmfY=m(pHF}$J28?)jH^+abN4}nf&gS6g=o7yYw!O(A2f8SDYa@HTi2b;uDQ1}hMq+tD zA1O0HA*uidHtwACP=h#C+op-7C_%KKvDf`hnW!lk(t@HQSA8I44+8#1c}E}-F(C$o z2DLZ!w{Td#BN?uqj#%RcXwr@KrUr-m!!V>%FoTXNK>a#I2p z2;bciOR0NoM1LVZWjq{K1PVw@ybFS1D@x;Na9Je2#WcLbM}sG^ z=|I77Eaz9_8JT$uieJ~fW|DgL7$c>O}0;59;E|33xPb~UBH3I*>f7h4S1(*}J z`30^lL2o58kH8GrFV?`wm3jHK80<70l}zv)~(cKHe47qG%+_y`BA?GPP#H>Dy2}Qo<0ckC&91I)%F(5F(G)L;SC0c`eJ=HEz4UiB<1%pYzghQX+beNlh{oBr?xq*^w#-*mzQ&0t@xT>lCcw*RQUS z$2B^#E~k@WH=MR;Fo}g>@KMG?+!RRJ@?i0MKJW7~i_g4BzFS9MRdw%GW+i$rC>)sE zov+CnD1!kOV<6Mtt^nCPtq4yTjrd0(nph!&YknHU=}%STHF9i zwNyD071K+2p0kq?TcX5ZAN%KL`FGs-BKYQ%Ea|G;O4nQ36}cgCIEdCJsH{wK46} zleLE>-h4K_!eB^3o;-QtOJdQpJ+h>G>GfSI6KK6h@za-15u1tn%4y+_4pR1%^i8vz zU!lEaI9mqE3sSPipH8Aho7TdnNE64frM`u7noBIk!q6oLpuSnp+y*! z4v`g5SNcl3eK=uTEi;JVwMv4W;X;;D>oM;$!YFuINpLs~wY5w^Z=W&ZZP>rV1aFKw z8R~aA@%72zGm&&Hu$A?3FLcQVgSJ3g1WKBQu z;|hj$)}s4+OZDJ+uF-D_&r7IQ?&`b80vQZ!G1U~g8EoNDgvRduPP-KkK(nXs2+Sey zy??50lx_@zmJvZow>b;jk~C{N-UGv;2@oSDi$XhzSYunyeYWGrgC4NRMAn5b0g6C^ zZu1h<0gf$R#WupSy)8k>HooQ8hU5`Bhfcn1&%#dC{m&Lj%oKR5o6o)=$y}!Is7EKX52H;RrW%%E&*wwXp<;W(D&n&bF(+@KqoSge zDAt= z$9<>i--?n^t}1`D}s zfsWJ|D6#l}-z!4g!?nWysC?M-`WzCo>N(4tP(yCS+7xve#8G~W}5n${73dQ&AO7~^z z51#(F1?TQmqWA^|mc6yN?2B09JJ~+;^fXA1Lod*Oq*XpfJlxq$M$pLg_{K?pJJX=^ zhwxsW*w=Ti+X7NS!wX0w1QJU&_pPj`bCL#ylOc$&2I=NG?g z!1nDL_%0?%`P5to)2GHuaG#8XxfD%*i3ggfYCA~7Hlj*H$1e)9vOx&53t}&TK$90- z?txP_o3+!m;sYYbwy0{5qXMfpZhsP)&UA^Xs;bJl!%t!^>Xb&=sQo8Lz7cZNZRL72K&aDa*g`>_~@;QV8c zg+K_^nRH%7Y;=v8hLx>%5kU3gvPL-)8dUZ^vyCG$)6wAn_E>3^z!qOLNacI#TZcm%M-EgKje_n z!UEj5|fZ$TB;zwf4 ztEl;wk{BK%k&LQ6SL;p{j^)g!R7mW3kRp()_9GzZv{m3R8^ae~Xq{U8OpAl5)S= z`Z$nto#oOiC_*%(9uT{E7VP?yqtVY{zSiY4c4xyLiMa|603g4jU~#!c9$8Mz$&>N#6#FI*UAu~=df21&fI%nwm4J{QFcJoYmf*o`-(@Tr9DO~_0FtW(ZLU3FNv6YaIOcn#R% zihtbyCQkm8#uj)YV^%)zl$dfgD?lX|>`m7^Td1R_7lV-Ml%4;~2_RG%Yffg?lVJ{R zb-v{g#=MfNevbdP&0y&tQjZ(DSP2brvi30lnS3Lu?C6mr;WsEtI3gU)@x1^MZkH}S z=3s7wzE8vzoVbvK5&!vlyU2KI!l0SNa)e?8-<$sKYGo%tDgRMOhA`N}sL31COVSIA zs(t{Go=f90%*~aN97mWb^R9IBxxm&!DGvD^mdGY)t+MF#xQrMCqE z%8CGLk&=vrmK3C!5J^5l)1(@+4N$>eTN+EL2bzbE*=mtE^^WC5IGp%^w9S*A>KM?& zR1&sQH|;w4mzI-1o(vWQ7J&4zG>sGWb#;pS6*A~yOrFpEr(JiURhSjC5@FYlUZBGBVej780HJ3 zKE2S8YZrvjT(~4k1w=fWgs`~BP(!OEQ+!zk>e_xZS$2vc<7L6f;@l~Y6SK$ z&jr1|$AFR5Gp>3C_y`ns>|y9l5nuW-40}zQCRCHGp#c#_YtqXdYTPI>Op2gLuTYH^ zf4ss?sb@0Af;V~#%y!U_qzdIKMOn2d%~nNudA?>tOIf5#cs4>kg)n5^JDIvUT=?n} zgpogYP4c#HixA}QT5cf|T+if%zKD!m13BK&QO~OD@jDb?N205Cf%yZLln#nP-kL)!VS0`c(RNiNfwSEo zl(UMzkg{=e3)xD1@nuoGr3M!LY&qO;tpSTPzL)0DH++%|roqB5AAmY@Y!GxEX0k_G zCY)}B(57`n75a-0k*HW=)Z+X)?u^3Bqdca)SxQzI9b18p5W^uzA9lk%X1l@GLu|)^ zHfst&HhTAjzMtMMTIjFK7`3Bd_GUq6YIL{E`MyBR0n{vG`lq$|XJt+57cX*vz+z<0 z48Yi~jLJy-aJ~QCdr*BFU--)lVn?6?6rmT6Y>-TsNlrEd z-ti}^hhUiZN6e(VOtn8P3$W;s&wtBebb>{BbqE`^%e8CQ20OJ3uCGQTO<7A^kSv(I zJ^P11|F`^aBm7SGlgvUVls_~7;%2|qmDqs^h4#7_L?Ms?c@!osek@Z?IGj zb*Azig>kWoFW;^im*+BlKA8TlvLCX9+m)?hlK#h7N?aXm}} zVaNCZ&-+%We41S3=jWSBZ)%`pYUiPwtJ}zpVz0l`&I39hhEEH?%haL%Y-O?8i$V1l zGzmR``3RAToL6TbPCw}3XUgbTv;MT)w=f8Ej{zNz`M6LNIV2bQ8#V%rllabwt(*hq zELoRi^ z`^{s#0w8{DU~TU0h+=*@cRz}UenimV;DfDF>;OwSR1 zNT3U}Kzq^|Kiw=yPsw^9xO%!8VHU{nbf$s{9;8Rsxl9CMH`>a{ffxz1#4|S{&Fs&w zC5;ZxLM<~ka4tfY+E0V7)^ogqW;;L?RgYPnM(&AmcoXPp}cyonQ<*iUxYHO*X!5n>9}*R9xvS= z0S1;1+c$M!MRb6yj~!A|Bqis*VpexnXpX}U2CAXNhsr-tr-5nesF>IX4&n4)LFbf| z$QKWl+qSSj2Hz@s|FInTUCW$8q0Sk|k0Uuyo)lniu^*XtX>fou_zoE%hfm>K&f-JC z$&GNy-yE9PPw17X!!GmVKmrsT6vPA#kT7(Gfz`f#fVvhSlQRN4RyJWn1ka@#x(J&V zkIYFS@R_Ac4fvBe1MNYy=?7|9yfOC)5jqoO17aYX2at}0cITVbBTP4)BS27O&30nizbalYpermTouJ-FeIc(2%@mReh zvGQ2!dJ8b`sTHJ~8}z{H5JiEO@9DRCmYd$bMAmHwd5+IXwk1FdHUxBVdd(fhD&W(rM$Owq+mTaS$QBOU3FkbSW)4g}DPX_AtX zp8eJI47NI-gJ3+Cp&1RtS|Tu14K7IvAs?(Ds9srdr%F6I8h=yKK&M|-GF~%D_A3f_ zvNF?((!{v58%fQ9CZSBPDiz_9hru)FfJs9EoWf{Bt7h z5YxJziCnre48YS$z&T8NC;?J{V!tY{Voix_c{Bv8V)TyE)Mz$fYuY-+ji$ zp4goK)$1IB9lIgt#da-`=q~L1Iox{Y?0UBXwhu~i6EO!Tq5dmDxWxPoBxbjH!mp`Q z!{J88km0-5b?HvTo*Q=91nk|cD)H)rwgQ^24UIjyh9S>VFSocEePl)$ z_H5D3Grrq>3dXXOA)TY1%?zwV-)uiuVm@6!!Rc6+VOsE@0V2{8y-T;{xnE51 zQSB}40U93GTeoh#RD4#?TQbuQT+{hnUfD*Sd#WwdcP}l0LpZX;=$c_c)Pig@Y!Et+ zU}jFJ%f`O)1X`{-*Z{kK)Y=`i<}e1%9Cd+9(o zzU$agbmOGQq#FW`XO;o)sjq=2+aa(z66>bS3VR+pau2OHk9`Jleu#`19-^9}p(`M~ zNw=?!{@(0+Sk1XG77fd;l8+HvOx8Zzn>;DUFPj$12lA+#&As}CrXmO^!Fo`_9||5R^6V6c^)E+iJU?s)_fCv}(`1l5qB?6+ov^g`!Md9-HaZ*bLxDjxcC;XzH zuxh{e3R$IS@S-2d8e()w&ngMqL+`#S6b8clUMEueQe%<59UMPXH4e>9M%w_9TZZ|N zoLl=ZX~;A4q>e{HES%a4dt76mS%oTOIwD?OO|RXOh=}M1P+^Jjxqk{uynWUq!(oSL zD>62_hZr)8qt{1)aKmZw-Lvw}p>JYN-}(2;dqmw9feI+Hfc;c#9vBq<;lSaddwGdei8oZcBResyVLXk$p5CpBpJ zSe*M|WP1hMPhMQ+xR;2Z#BBas$>2v~A2mz{%8IDJya+OK2EZINU+(y7dA*E*OpJ!r zHziWq$zYs_^X{aY!J5N0m2rn$kFn4G6J^fCB7vj68=f1Hesv#GOz`D}-T}HV8sLIbnX>fQx}P0p^1zOM3>Gr^)qAr-Ym33FXi8u)g4D1i zR6+L6eyEs~(!Hi0`m{1?^ihH8mxBKb{sAZUm zegw|BPpjXZhF}(9*AjmTSsSv#`no%Gpm%?h+#aMUHq(F}!tP`sF8LI@8%b$Pap>w; z!_++A_3;ia+eJtS55e(--MdGMsN^7ky@f*q7aiAtC@xgjWhm71+zY58ivVVE>m?b2 z_!mkq1vYoeARt7L!N}L~gcna*pce20k|fe=bi8N&5B@~c5Hvf_@tY3xCh<34vgyu~ zy1k3`GNJ7DoxAzHl$DitUyxIK70aRdlt)V93vWE6jJv-E_fgoX#RDDJY`5#`RZ6?Bemp}7>%DO39jgJkB z?jH`?mNFwj;YhXtFz<;hjV3h42|U#{FqmBI0W)^R^TOv3{GZb_c)>Kg9V)pkDR87m z?;;>gg_ZpPNeNZjh3RJ6 zg9i^uUe+J1pDBguUXx=R zxKA3*`@K}^O?|3+cNgrYnkAtrr_pmz(PS6bG5T3}ptoU}MZKaOc8+9B!ql?-<}u>rWS32{)VQd@tAD{geYPPc(H%`AM9JA!`nd5 zv`j-keCccI)V#=6`H3LmFa`h+j6*c8% zPEER;yXSV~S&wA~$-{f>_CO0PT`~|T*2bRkG*i`Ox=0bkV zU0tA&91AxJq@6r`=01|IIOebjIkvK8>D*?4{DEP+fZsqlhQ0;Yhr>Thu();;bxx)sq0uwW%DS%(`E3 z$Ot0SnVw5I+ysRAN0_2T6k2vm{_B0=@3itVp-Kebjdkm*#&Dxya?&3&D9@3}?^$8y zuD{EU`c>5Q)fX=tFUz985Ba-Ef10{2lnCVj7x&()dn|gb7J<)LEW;t>4u68{Fi7yD zk7Be0jpTw|O!QY@Q$D1AGa9CLS~%Z8v+d;g2|EC_%Ewh_A*SG19Wq!O@|XLvHk&HF z0-n+?cX`?`2H9vh2HbOA3N?4#R#;GsM1D^`5w{3gMOhbVmq4YuT~&dzQ?;V3NM$Xb z^~9Hv5%9-xngEa|Mhtw^5-QbJPIeGeq-Fuex&FSomUuR#5*%yLskfxZP2qO$Cu3tb8Rqo7d*83C^bI4MvglZ% zMZnSF12fGxb$~L{a|s}2@dZXuYOk0w2Cu=|i|Rf8mZ>g&<(`1OmB0QJWW~gbK_%(q zRVVc=XdK+2YTfn+u$#eH)9x@~Af=7}xFzMgEAM^;nevuuo7$nP_br5SUC3by@b@dr{w6_|Z<^dmW(Y#-(YJANNjAI3L(nb;sGK z($0CX>w!iLv{%4t!aTQ$l=j1~Jz;reA{edWp&K#~w3v&+61wD*4PL(%yvzvJjyvpI z$avQFrF=OxQ9EfKc6ug}aTPP=-)%VKY+c-cTf4;zHeo2L_*%$udS;!)ra|kQ((^Ge zX)({!nM#hR7#wS%#~wdSv*)L;WyGsjuLc-v1kA*gh8i4aClum$5`Cc=5u?Ob-O3uq5Z_2uIV|*!WhwVZsi9ceg8I=v`x6SINs0i9T+XEsxZ3xju8WY_9v7S9*_kS zD)(TRsxt_Gd&}j6vnUhBum`TGoi%?uUW@+CG7=EdQ`Fv!MED!2ZT#I>2Ml`e3u~Xx zzk1*ykx)X9NiT~?jAayMk$>eVl!Zge0X_L^&AZoM>r4XNAQ z!NRsdYA;~+K5Qq__O%DSkVA-CKkeYZg%+C%h*u!YKhffBumZbX!yZAW&m(C{=2y*L zYctwG9wnUjh`zm;ELU>!iywhnYb1m7XRO8bgHjJ57K}>*{zBKb)h{D*rP(#hKjYTM z;sR-GBi~_ldWAh%bsf7+#sJF4@#(rk#F~VJj~0L?l-xX_rrQP`Gp%_t4V-;(&pz0^ zpq8;j6QWENFv9jliS&0cab#zc>WEKL$LZ&SRX6RgR4#gxBv;2 zSCu8l{@tJ9ZWeBuvctdS9O}qSdL+a+=I@$;1~}#kg|xf4#;wLLr!SIQSFdIUEZmU3 zFk7+qwW2LRs4x7Z7~RX(p+^_FGJ(L{0-7(BA)@}}EQ`LUfA&ogM^^FSjN2 zi?|*~rt>1$)V^>U)IBCQ)EymbmB+kQcN8pD4jk`;NuFsXTUWJvLCesw`k?P;wGda4 zm3n=z=km9(4wHLs-3gf1^o*>?Ioe<1vybT*L;bQX)$qMMvZ}r9 z)I#q=@hR+{#Of;PfaUvvfyo}wsdqP&YX(`6RxuH~Ry_~IhRGF685MPbVjmrWu-N!< zb6Qn;`?vOa6mh|^7Ic5Z6)t#zh8gj$>N9# z>SY14Yl620S%f>D)(wL*3?br6xOb7E&3_G25!iU*rL*QU(4S$i`50aOh^BIM#5>t8 zc~Y6MjFUp@))QCbzQg-mU?t`={oS&tge4JT??6;4B3wUV5 z;PD;co%Wmhq(?gGoi_R3z~7WSjl7zxr13JpSg*9kwy;xkfP3{r#p*@Z8nM3C*`L$- z#|T5;P?&RUU!A`16@9o?=SnUE1g}3XzmO3YxKXGw#Q*MU-tzpwV*-SVT?h2`BJB-p zVv5lmaEf=Z*U!Z(6J8~l0_I~;x4P=bcNijx4x8n_;Vzq24{#1XyW-~;@N-Bw){$RZ zWU6|4wZ39S0esoRJcD=^T{@Z|lfvQ)A0g7Oi96HQWvi?B=2m}-_*~kfy6PIWCk#M> zH4B%{yex@1>5`b+HXK~N1_kpJ;DHuD1*Ga0fLU$mlFML2Xdd5Wt5ZnvCj>QgWM)$_ zwU_|nzMfC@;xuKjbMHnMz^!IQ6%aQ}XN|mSw8#3&w9Vv?&IEDI_+t zfmlP8(s0X#3rP4;PB=XqUtFu^Qd&OZ9kHb&PkmTvtRei0shm2r>B+eEzG~mkrYi_1 zecrNQf~zTtQ%l<-+Hv9n|>*)P)q^Cty#A;sfu!sSX zHb=|_D{kL$`bkmnuoD{CHwrxb4r>yY6M?jzKeKGUhjYOVWIGv5t`J$JkQF!q8^P~; zynep2fc-l*&p+vbT{vUMH{5doAftAJ{T+J8@beCAH|FCO8_Za*FzgQK0pn`#9tCEUqil%NHsX;EYMa;8O~ynj zW#LP0JSGkY7BZw|?cwaK(taTxD?`x*@z5n6KKyrX-_EOa;~;oVd_lTa+*V~VpI5Pa z!cb-I2PExdr;;w(JU>dlYnOk;e47Hd2|-h8d2d(WOzeX3ejcOcpCWe9RB|pgJi^wi z$08*O;r(}C$oBYVH3rPWKYoq?F0w8e3|7$jkxxlL*tw6Q>FIFEf&F--Q4Ly*PR?h+ z{rL~kn*gMW z%K~4mEsI{#2bnqcO{gjWmJrkZf+m=_Sn=t*=M+T(p`qVcR4lx@JB zQT!?FWs^oQ2~1?)dZ9`;1_cX`_V{5PtIW__=mh{a^SgV8%4WPzi~F~>wsPIC?I)oK z53Celo_L68&y_GhYr=Z42%x>YY^+FS`Ue7f^Un7p843$8B%6w?gnERSq@xTop!)UI zF0E2DNyJvl?~tJLL?YC`+>*hDdFD1c!XxaPY;_TX99q77wOV1<@I8*K@SL}B7mtu5 zoPK}VU4`5weUZx|n1zMK!p_dF$`nGJF~|xW^0J03oSe!xbuj2oM6RsS-4jY_PTc{} zb+8@`wum66p-x=ZgjH3`6*$|pGk}_Rl**G z6AyjIgP7>uvp8Obsqg~w>>IU$#&7Z+p-qZsTri7c9zG;L>q~mpnkCOo5+vH|jmoE;F0ir_c*jG(TFJzI7`gu-cQQ*oXg1M>At-Udyf>LoWrGEez^DL zQNx~$rmZln+0Mnkwdk0b;f~MpwsW)gwFFDE-dA*!0hvWmJYvmA zbb%6RGBSHJW&?j=swsuGMY30-&|bxQ#SJDiQjkcAO5aaD9KtILQFxlD`4?aAaU{pB zPM9IaxT$;%;lw5jALb0CtqDZZ{3(pkd1nEe{Kf$4%Bg7c55IW1<6z<* Z*#|0-- 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 Uoi31B0si(In(e!@$K}HR0iJp>9smFU literal 0 HcmV?d00001 diff --git a/docs/arc42/revocation-service/images/verify_revocable_vc.png b/docs/arc42/revocation-service/images/verify_revocable_vc.png new file mode 100644 index 0000000000000000000000000000000000000000..e5b58d88bca9d4628af334b6678843476d58995b GIT binary patch literal 36718 zcmb?@c_5T~`~FnopmI)9ktn-tktKVZJ^Pl#RMuoEOZK5fTI_4Gj6%p(gY1Too$Q7z zS;N>F%NTz5(0RY_`+eW<``52O&Z$|Rd7jU6fA0Ibuj{&>ms%Q1hiRB-5D3I!73FI> z2n0ns0WQI6NipVe|lk&pO+5U__d2_ z>oTZa@}oaTWvnOu5KZS99`|4_lXyPx&z<-(S`}s%H_TXkOnm%+r)bpunMVSn`T9%- zlwuF)%QKV0(iCb_{CFnA96mkHyL``Ypo-SPI4@Lba;lNDu)?(K2My=rXCB&S6%3s0 zHm|kb9iNfaqux69h}Hv9@I$If{ASUS_w<5yD{;S`&NEyUorpi#!}OGK@a@A^<2H(y z!KNaTk;il5zY`@3)(36;9lTa;g|=Rwbf|YOe%{E|E1;TIaAOB?CxHC`mFIUFGj>IT z(xmq?Ow-(>%RCcbhjPw@h-29eB9yx6Zz{)iQ#=vjvEn$CZo9zHofjnPd^lkA*TI{q zz{^PLrC!%CeOf(~`QT!pA>-W_jdY3Slz$!a(|?z#f7R;Gi#8g{KtM&e5p-j}V<&gaT>w<4c<>Sl8d_+kW2R9ua6MVob7b6cdwl)Cam^R{_ue;WoL>2Y0=-~>#Ss$(1v8id!|mz-6`7_q&43&GQ=oRFYZ+drK> z7|If|85ndMJ9(LzXrZ(eYQ@lq~JOG_X1GrFtjEBqC>FR?s5LR$mgHYUc<%4txH9CkQx-A*qevHv zB2gY@Qoc;NhR3lxC|tYNn@k(Kd4I^H$Z!xZ%j_ZBYQyDyc@xibVcI)ooB^SkTvE%o2R5Nym8RG<(Q+oIFWB{oGA?f|xAHxW6}S7Slk#c>AIu_qu1&3I zmg}8be^pe#*~in}S;obuAI=VMZ7kch_YGG*wP;FK34f(sAa2)JHw^Czh(D?eIV)1e zLBl&gSZ195CRvU#PQ>C>1k#7(LXd7`Cu8#&=JkBX2yp0?-FaO47N5d#IpKV&UIo*|EWMs5-$8DjKDsWn> zlcuDoh*7N4yli)`=p8$HCE1wy^y#diP)xwa=1flH<$7)rIGFsor;{!5;gwwKZ>>Fh zOCHUNOh4*Bq$)qy-(OCIq}DBd|!TseSDV8Ov-n>)rEGi4As@Kf#U)?lyRf8@+w>x#9GcQ^7JzFG|InCYm6rsQsBgcxjouqsAsC2=hboJUq&QCE{yz z^C@3#M2>|i%7+C*K31LTbvgVXhy{}tg%F%iuGKZ+dI|yvuPU}8xRlS z^~nZ`E<8GZoE434_zbgop7s|w^kh9JdF&%6*R&hOuJeMYN7B-Gh&=f&GX_&P{GUHZ ztqr)g7s$CSBvORIb{qc7b>B25PfSX?*o}Qkrh-72% zr(-pkX5aXU$K}G>>ppo_*U-?Om5aqM6+QInPSxF=eAlFc>tCkl zT^-yEvTqq}FvdpM_4Q|)NC%g&pw@pvYStlUbD*sf+&^4?N(p=4q3dJh=8qF9sv@5Q zB4#gm67Xzd-^~LR`i>2uh;~pZIp3;^J$dqEdnHw#w>7sd>uTt^H~^7lo~}?mU@tb_ zYCzU#4T%};rE(sV2D**uo*bX$3H2|Oy9L~rw%Bo8_3qvG=Ge7C&)_V82v7}&pG06U z8$ywi81f2_1;Fs%EB?QMf&YRr&M6Zbo15b$90rqBc0zi_>cgW2O+4m)ND_9@Nue?! zJRmwk^ig+ak>{G__=IUxPks1?{MG}v?#y5=jkp_HS``Vq@ME$CCH)A`EIcxhq}0lr z-Z~K(@BOGVO&hW}{t)>S54!QLOJkbsO694so1KuCqB*u7uI&PuI5A?`YWF8R9^IUa zF4c&;DDiOQ8eta;T|^T4%e{M^97G>u+KT9jlVn}|bz=8$<@o5hExIK11`bP@Z-t=a zEukR45ywvqWRV@byAa-k%f)IW$i$fJ0xpdxl42l}l`Jt{jzqf$pnfk*Z6gY{Z^2TC z=D5r7rIu3n(Ot^goV;BF+-NzjH9AM&WRiwSlKv zx5-~YOg5tK)&`Z)b6w{XB?dAsv-te@`x0Mvb~=ZzN5_+vOXh?6^Y5RK-i2>*7l|+9 zrG3`rcP3{wK6 zh@?6aw^b|uVQhSS@tt3|1r|y#3J{+-yRME7pn)2DgzpHWq@$=eyJ=s~GwN{i*P&`S zeY)5<)0wUYH+u3atyKf+=t4mGPsxWP)p)!ufdX-?YJHJ@>f~a3`51fU!^c+wnZ$yg z*1R;8;X2R~L4ssl8_epkG6(Ad`8)xQZfzk0Bq0oI9jder1$&PBoyPn44b4_s8k#{! zQ>cU^P)JP8k?s9f$((MLjI-G(P@5- zQiq}PmTl!T6@sVoZ$Uw^FwV2q z{%jT9>S|zcoJfh7<0PuICh2ET&DPd_`|@baKHPrb`Z=#)yU#IYZjAgK9L`nG=Q}F_ zsq^IethBMYG-0<_O_CJva7L1vQqK~9LZR^9LFU_ZjXxzd@Li=lEj8bvF?>${ zP%agfgvU(QUpSon{Y}wa^W?c(#RGETQ-R%gJL8Dk;l*sRi-%8bvdDU9m^4;Cm3Q|f zZmpXINm*7tVF;FKzkJ%ZFVEd^sk5%m+9kj??I5k3@X#zi#Q*w>i+fFy|ZWEIaV}a>A6w2bJLCs%;lI4 z$jZpv{T5KfWpy&?s=hm8*4Hta58ie-OsdnQy`0>tO=sF!`o^EO;}J{S#0y(nTLV9S z1oYWE@%I;WZdEH2o;sOhBO)TgXl#$NL!D}1+3$PT3OiaWqKL<4f5s3<%WR9XdgjIB`F_j>edxJuGZ`L)OnQ!;#l~h z7=`V_fBd1-6ZUp9rHONgMb1mH*eF=8Z*jIjiPu5Ev{W_ze$A^DU*Aj1cq@^z`kU9^ znvF}NgXIS;+LL4Y7B>9lOB3VOx_RQ&HVhkxg!qtzq0eTM76I*ck%tARwB38mpY|VX zimmdNk1!&HUrRp1-V|Aj=e@GB!5?zRw z_t|)SfE&9xJruS^Bg;G0-C@qaMO9AdDs^wvPWonL$r8<(d8>%7m-vy~O~Y2*F^RxO{c?hnmXHcINIPLqOStthUJ*vy(_Ndws8 z%?k@$+#0uZC`U&|da_C1K0L<|{AlI%nFiB!Pw+b9*OQmRv10ed)pD}GAGX&$?{EZ1 zml99vocDsfXhM|iA>BZ&v|X9aa)`OUu^_*2>6D~$*J4=5&4M7^20EFAkS2?51*HMA zYRm8D498o|#O`-TJns#bkf-e0(Y;_(^5OZWfS8dSj*iSeJj4twj-G&SxZJLvZYk`^ z(c7IG^#wzgsK!9~&b1?$0w$6Y>Hq(@jcR%DuTer!HIw&d7E1B7mc3lQ~w}g^jgJn%6F)eRp9wvV` zvc;eyx(YrTy7my#@`S>xtvg#Uyd5+b;A;N-h6F(~O2xY6BSHWCh__OatIhIe54N0^ z_CCKKwEnyWOy%o$li)5MIi1j3_^_nruxaT;2nl)wP2RdmUAZc4ZS@^O!O!UuF4o3L z^iGHLd_dIKeuv<&+(7N~8M>+2n>!baU)GJN)N>HiGtNsc*qP3B+ntizDoah6=2g?( zN&U7doYwzncIU>_mrnBya(O}^pjJR!74K|QjisROYkN0#bSw{7q9c}4(b=EFNCB3g zH(FBRq99PzG!i}<*B{+#NhR+~-4xZxxVX|QwQ3TgXU6@1*_dIVt;t(#*R$;tW3Q$j zrAc@?YAL^AZp=)L;EBdtEg9rm3PZTC%6W0IkIsAxWY02}Pcq`bA3W$s-oxMDbu8t; zB}ECS!LrSDraPh5&k`Y{3f@|u0=DaYDd#ww>`aZhM|l1lQSOU{Z6h@qokGtU1EZt! zq&PVAJn5<@x}f}+6w8pt!Zp*)q#k)W4p;PSuh(u*zquk&7&+0~6NvZMX+SM`Os6N1 zf$EAGZ_cMr-62Dj_9xY>hH`eQhIxnj`OcydosScI@EBpDfq9Xt6i`O zbJ6A((!ULs)tXZSwtD&UrNB4dn)2CcQAGi*TRr8Z#0JBs}D8&stnr$%OsL9_)Ohe9LJyOC{X$oDbub3CxlN z%k>WuH(?WQ6R{`Mu4mmU!Zbe}wfi>Yjjv8;60^bMoT{xe&PklD4Twn$VidlcSyEC$ zxO~jd{sCX}=bTfJZK6q0{3?-jS&?QN*zNnt%x{)*9udaI(p`5DR~d-d#WQI0aUo{q zZ{cRRt&_r1dg=yugo!GY_j|Ivo8Oua4$7I7JR15k>wh;+2j8C^uUL>WaVi{7mD+@Y z_68c$RiGV!76s%2jcC`s_V)HU?9MiDSpI%~fk2v(!hJ=6XmmPo-~g1gURi_|9&cEl zQ-XYAGotEa?7qDFA-zJAZsyyFSu{kvJ|VQd)F=cHYU)Q( zt_W(TJ4;wtcxgV8&ya&vZeuKbv^Lmqa`yYXDrnGqB@n~6a5;uKlifKxgoU3M?%g}} zJ}LGv>vlq;Gr4ddV`R)Co^ksP5zi1w@}{Py+?ok_=tN*ArCsOrs4U)o3p|yvOm52Y zClL>d@e(f6%+$&(EG$2Nsw)O-h;Lqg+fg?5b!=)1_z#-YGy!t<#jtA0;T(ED-C3RP zIwflT{i`1>(3DvkXV_U}j zczs*|pW4*Yl5JA*MJFgY7=6JC3e7*`QAy0uD+WF`Ha7U4>y#kxGk^b9v^>(CO!qJ{ zGM<1vw%UWVYE2Nhtj&nMQ$-1F`$5{{TR^Y^iG|FyRQEq}8k(A2XnbJzDvrD*RYQ1> zjEqbVc0nrwG(m=g$~Qptx>+~#Apm-S{RJeyngtZc%_UQS@8;Ru32r0EolRJTe?Gg1 zxvYr9QVwpcX3mRfg4ms}96_|lY?|IIGV`}ANqG_sR~_E%BXdB7kLG?9nb)7&R4k1E z3YChCgrGA}PmqBK=wKGnSlnEwlAM~F^0K}RC1PuPV-i`qz4`sK8TZ1+;heFtHMm3* zXxuM9z7j9(YTuVfchQD&TLy>+=McAKh>NfF5r}98 zL>$eoRI&Fuxxag0w67w4SF!lDuy0rok+_x1img8Q`}GH;|69@cw@=p6yZ!x(|9a#K zi-U;N{bX?@v5Lx9cc1T4D{AZL3B=y(6|1mt@aI2?C;$FQ&Edb#jjXc#&yoM<;(U*= zA~X&n8sv$KYPbHr=$_WyruskXH2?NVna6*h+wM92r)87!LR@4(@a*7gg&zKW(YNIP zZ%GI~Ik_KOT?SlqoQHy+;Fl(*I+lkqCR&eMV)0D1RR>teH;P%57+Fd0rdHx)pwlRn zbLfL$xAlgCi9-=LTk30bi_)2wd@bvD%;u##f3K)vn9_&#%WaQz3A6ntSXfZO8GNCB z5qijv)fe;^Z@SJ`CMA1>Lj>Cc^_7}xWowIvt@C<-B7Bos#_bcZ1%rcwkUW6*hW*o$ zKQo`JOR0Uiu&Ai(qyEe`ao%S#-n9iSKYua3FzwO&pa8K9C>ZEfcUC$y=gU;NVzhpa zj&=ZaXIBa;^y=jc+{2dAPY!*G72=Jj-4X=y14 zS#@u&oX9vHz?4=(5&rxZ@P>gBgCPR^FV$#7tTC5uuiVtq3OSa%pSU(}s3>^j(gHV5$3bqIs;Lse4VsU+?Tz*@I;;wm1=& zvqytTTU&z2a$BsTj({(k@5+3Lhf@aB6@{D3UD|nwt+DUTu$aQDh6$@&Q*4ZuoBM25 z8zASP@p;Cj%wUd*RYML6ed}%v&Ii&H}KF{xh$*}N* z9khThi#+Pq>3~H}1I4LZX@RP?%2Gb@5_##is z+V-A@cKdknAF&4V>%&hQ?8YFMP*6}P`?<_;Rbqe*f#o`BhyL2C`skFZ@tve*>pUq* z8$bb|;ml1=7ToLLvPgc$JkI4*kLt}MDjTGt)C>yj6>Brw{z~@TJQrJ)+w|x-0_rZ# zq58#NaCE%}07>}X&t?O~E|DmG;1j!Ap7rHMKLMDc&V9AyPxl@<%DM=$2t<|Aku+NY zV7DG+%A3|G@pdq+!%TEA`LXI95YYD|jrLHJT+2Z3nc_#Y#atXKD zRXW)`kSSC6r@!PA!UY^q7=H@n@`XYtTmgqBpCJ)kY zgzwo!#jnq6*uQSmGtkp7BX?GjfW_LkHybU1C=B%Y>38DQO6Yl<+ys(cbSU?zVMJ{B zNK?!Ozqun04h~{ljVq1;UZ6$W_2p^LOGgg-t4o`1L}e~<>Le9UvE(W$h0IN5qO2iBBx$r)oPt>Z@%~hliL(tkfqvoEs>F0&>5E3ks6~YG4JLGaH}syVwwr*wo}T zqi!#I%FlYFYNCNP;hvIJi?#r@5SSV*V)^td^ZhP@NmGHFIsniNOD6gjAv@8?dJ^;! z{CRPjMF-3}q}no#av3jJDtmVd`w1Elubvs8WD=-MJIk&Zpyi|0`Pocq^-FS%H1{iL z5hy7rcnaPnrwUR}lGcU_3L$)gnI+lO-$=Q9?Bs^}2}-0%vGc=hlbl!*4GqoBJnKb3 z#%dd{h03kFK6U}hrIlrKj{XAS%D4f!siWM!U38dZs}GjjtnC=>q3<;!XbW;fpPKHpoP;cTsO6EUS%}L_TZz~w+6Mxo zZg>7Ub!TTa$H&es`dFvX8wOsYH;Ch~Co$LXrh|7c$Xt8{D)_g!{yhXY|=WFHxi$o%kyuH0WSEpAY z$K!@5lXubma5XMR)T0b5BAmyt1&oyi9xO5WX#`_W9vcy^!{{0Rs`0rE>LdI!qt-p7x?|Vmi@{;1c)lZ$dUbs%!}TeDzbPdCU?7v3L0vSHBfPn^-!^TX|>>jxfk~PuHx4K?{Z70 zLS|Ma`5$6jy3DI~{rVQ-l3#5);=8J{GT9OqZXr*teImYN673_nI+S;hes?V&ivS!K z1#xdz*@G4h7tBtNxFtd4MUq?>@TsK8EAQx2Q&Rl={lTnb6m!E>LBYFGI8w*2(N;R5 z?iG#6i;mgu_QB4z!ppCm)7q;j&e;=2K@8WL!{r=u90uZJHgPkDL{V8;sbh*USE+yW zB(ONwMzg@O`7Jc01LfW&PoI^AP5VT*->pZjnP#;2)+h|zB~T?L+39<{@nyFpyT@{_ zVUUbgEG}EHYm&d`-P5I_J7g|jYZ!!^AEf&>R1`}?zJKGfq;J8jLKh6c`~jgZ1O%tO zeYW~wt~_Iv@I*TpPDMhTmpNYXhq(Y7+n`=x>6QF&990DW|JAEDY=g*&FHl12u z0`L(Yu4QD8TpIsa57NgQd>X(J*_D6&nAzFb$Ol%kq7G}lI~E>EAi0GYhQip!;OM16CG{Gf?X+r~zH`5Q(UqQt)@|K-a)ylks{* zRy6!&iSrbj15_?SDXE(@kVJt&h`a)zfYu~PWW7y^751bW4GM)459eJYC7?F%S!|(q zwyYSpAiDuw!RrT(k9uVbT1eW;l8|p2FjrIfE3Y>kb%^3OVmrvFcl`aeaqJFhz9$D+ z;Lt+$;tarDkI}oqdrAOl7onq)uRT#|>?-~1OV7wT=UAgoh8TWxFqOfTe$-6`S$szQ zW>4bf1ND+f@72gh`v*cBe3E*FD!tf~laqNq4>c^g$XEHzJsh|>mHoBhr*3jFXx|Xa z71|i|G&FOVXB8C{A}y$zBPh-{B-rR&4$uW@Y(v$^*@}&b;Mh6BD)-|qp~Pc#8eW^_ z)}^4LLQA=*G9q(KWoM0{Bu0lS1u=&X7G2Zg1?Xqg0MwukVR2FWIBL9sfw!)-;R={N zgq{Lj)Bfd3*mWTB$aY13qk{>rp61A{w9`!jRu%+JD~rFr5!)?u(1b^-?n2M_XLKwzL4V>;z|T(ZKzAL0Q~ zIq`#=EInC~bLTQ`ZTMWAw(v@H^be}>WI8Aj^pmAujTVhj5PNgrPEmBviNFlWQesCe zvx54p zYV<7)`?X!rsy$hyfl}Bf+SLVsA7J*iC-k%Dg|^BumFToBZ4AH z?HwJl5)P)Y<`or}B!`$RHyV|tsjqhHA{JFhr!tam1dX**Sh%E)}A%` zl@Ju@mcFac58=it$~PuX5svMer!J`^AgGT*XQ&`Z$WN7mFrNX&DXkvM|pP*7u0A_H74K?W?1J$fGALr_1 zk#U4YW_{2#Fw(ErHAl^SRwI-I2MY=(;f)Om+6}4%ddN7R8ArbYrM$~3n`3v%24z<} zb-}M4_Vs6#KWyIF*;$Y=W~-~KX-^2IAEwwbW6+-bnG3z| zb(ZdrVi&c0FG$%vCQs&+2cB6D-dN5`4l7nUT#rJO{c*$-?u&jRehtbTnUri#QPT-} z`4U(7a1>Iu5VBmxye?$l0m`&o=jqO=P7@tBXin^k`=+l@(y_>Z4b8;_B(jrI&S~~* z1E|?014w;7n^WB5S-iR*WVY8waX8%3Su*zp<~&_RCIRI82tE9m$!RI)ld>LpB9-t% z(C7NB{A#;k)l$0Dz}KK0ZaHnxW&Km+@|7!BG&F+DgP}yNue({4qR~SjPv^j0S{SYb zMysztufV|Itq^c2NqE{65gX3H4-y}fwasV;F8Y1Zpe_(m2P$vwoO5)dbbDGDSD@IC z>bdmTOpR$3!T~aejzKDr8#mxMzM;IJJm0>Z*mq$N>x0$l-p*8;<;$QA4f1^O;8S}_ zl+V?xSHZb$A4bZ%A6<|~ucV*=*PzUcmh}|p;HWm=xO3-@&0**`x|qc6f7~cLo!a;z zrdVI>X5sZSKh=4WMUFL9RacuCE?71J1{Je`dzUGStn15<)aJVeqiQn8&XC)?mxRVpcD*0}Uchk?_p-VhbnQIDU{^Uv~(m*lbq_E?da!M&Q zUI6`}cQo7(zfC4w1Xsae6@fVh69RCz`uv<{`&>4b#&vFdu{DAy#M1}0^^IpiPEB)A zQEfwseFkBlt115gQK|RZXbs$tRl0WaWqOl66OE}ZVA=3#x$QljD$lxA> zEV^aD;_>SpP+olF3kwT-ln78oGh|LA?!j=yzLZj*9TecKKEF)kf^2}cI=?v%cS1*t z^|Aq=B!~-dKbL3+=ldE@o{>(ACs{I)QmpgFWI2BqMt)1ehk(|GRBK~j%1d`%4(Zo0tDAq>~kxjnKpmFR1_&QYf(9iG| zZ%E?cX_T!^kKu~(D&1KE5k>Sin*$_->q?0ORNms4`@om}krVW^iWuChi zlGLf7)0x6hOjWAQ`|Qyc=Z&_3E9*p$x!`-X%cwwHT8E;k=9GucykKJqaHOkZf#@^X z)oD_wojHxmO?}o5Kt?5Cad|nHyzlwNeZV|AT^>UHuh(U0jn6|qX{*~~7O3yR_G@R` zy(&c)XkK?K4Fu1~>2eT9AI)^DpKhwFdl3+j2~sKq`ZGQ|tPi-Ppz?uq5vHFK#aPLjSVDtvgOIlOv3($^^nfMO(WZK z+by7z8-xDXWgUXcJArk0$XgsJwsGi&ZTv^a@diR`d#4FpVpCI7Vd2Gl+Rn}^?-gEW zN_1QE@g^HgyBbL{fD9URot)TS92rWy{1li>(_iKj5-R+H$rM)3QrQDKN!=~or4+Uri4EGef+*k2de=Ev({&vL&^;jse&;somU<-r5 zQkIyQI6QoNHc*bdE6B1eyqj$X1Y@zp=0AV-1^6^uVo_Df;Wb5CVT60KWS-PC54Ymhk*TYBDpy&c-(9 zE&2P7?}8d%<%vSR-<&M%+TH>uRZ&&cXdbj`iE{@@9FQuA>43^AU~0)>uyn)pNi~EW za4q8i1IEW0SXt4bp+C>un_e1kgu46s#S2JPAyI^>4l8I(6kaK=g(bcc&Z*~DFzG9^ z!ouz`!J-Bhb+dsRK)s}eYY^l^G)(9{0HYLeD_GL31Jx1cLI_{L$%a`sIUi^BlGcGN z7q#uxdOrcAbI@!-M(GD-4l*cAb6`Qvo2t&hTTS)k3_)?tT-olr%-a+%Eph(*_vYqilvcr! z2J`;~E-c@i>-i__+PW)I(l1U)*q3fjXS6?qWrCTXjE))mR#;GSESRjlan*%#hbQvz>W~V8fa$mI!2<>Yj!u__FYl{rG;Cv_SeAw0EiPpre2`ZlNpEb zlSv`xAAq><-VzHYy=`D?il1`1fXZ1{Umu$7qoWh%k<*@R4;aB4sD0q|0j}vU7XuAv z`=d~sD?Ib!9-w(6QE935W%^(6#LaqCx7Ft%kK+D2pn*ZEf{C-v`fzS(=U<5$IVG|_ zMDRMYdOO3`)rs;+;WxW3-UbVA{*!w?%n!_KH(iH}9MU5vV*mMa3{zAY?Eb_QwH;$% zaQXH9YzkS%gpjy$;U&M8$AUsL3L3PYpq?XdJAVSnpt=F41Q^e+TR>hOc|{FULP`7R+;|l;pKYK<2TL;8F za3S6xFQ>3Wjug*^!Qr1eJkID>ApbR=t zI&#Q^7g{s$m#0V@y1A_bXR*Ar1ZU^6IHHi`cPd}`tRx38EA9D__*CiFD#kPe9_`Pd zmEy6WF7})2DDzySq@?`t;ln18Xk}p$W7suTj(o?^ZI?c$oYZ`Ok^Lt#F4+I!0RRP! zG`<)~24)cbK*4sB%YmUX$w{F8wTRy#{mpl#hcEr{9ol}tv{xAc^LvXK_Fi-xd$BT* zsuAz8G*%DJT>;}S&Qt9Q?m%J5@$#l8CZ3U&Ha>14Ol0h;v=$661`5}5aij*eHo5ia zyz@QrGK6$jyxQAO5YS+!YOIDc7Qb1j{n~?oF9N=a3=hJ{@F0xjY$be8?VYg$AOyHe zaxT%fmzKTHsRIXI10YXA%z$&BHe46EZ~;89=GxG|Kmx+NAq7m|UOMwU+AU;Mz; zmG1CiZ}1Di2&?mC!!9_9I1J-T{P!iuSF59|OZIvF{Q1|ZQ?N9O`On4ky1L_Qk_G(i zWvR1aMu9*e9HFI+yZUyso06eS)p63|cWRoaU82a2NGFcU8Ge+D?UN z=+^tY?+Qk)W@M_2T)}qg3rauJ z{?5}CtKrj|PF#-u=4Oi+}8ou+?bnR!zYz@>-0t9sXSo zNAyBsV~K;F^9qA0W5T?7)@^Kje6ME@c>J}S`+;#R>`W)O#6Y3}A7<-p2$b+kpTrYn zpB8!M#kVnmrnBD{2CYHQV&8s@H$1YgEw{IFJh8fu9ZxO9SS5grbQa#*y&B)wFu$d) zqEg@5YNboY8-L#|^51uFA*>a=ND}(T(gL2zMw8r5O6(mMh!^sm-MtBaL!HNPcm75~ z&g*1r(JzICJ&NO)AYaO5IZs;(cyaeupkG&?@(T{ugi;j5EU9Dq@Yg>>ia$U(=}2t_ zX#F~`-80=-c%mzQ94O6w`}UEkUPd1n4S|`?TR=E8fEtJI%7`u~C;*Mj%gakqkrI7% z_kyhKFvxT(`IH5i1`K8r`w07KJdz~kIw$jB_!2exXq=;?BM`~$AJk8!HlnWR+y@^= za~+KF9y)pc5>0yra5dof*Z|54a4axZQO}npDacXupqq;rsZF4V(W2M>{{A}9M-f2S zC&MSW9#F1N96Pp^U8IuMViE3>Hwsa6>g35^FvNOD@Ma#2|9k*7@!`XVF)=X&u~2esgh0PQ}k*Tk)Nl^F(DzdltDMheQE>jW}SX2eT7OI&b8>J)rm=VZL}~@!oIa09P6kTOOd78Vs9LRrMSXPhCq( z)IFG``q$daZ}+?QnxrS^04D|o-GmFehJs?z*Df;pX@}4DFxL#IV`X9CMPT4S;lrexoTvIcrd#gjN_if&$k0p2nAabTT?{)3 z4}-8U6%xvUC`m`M9fVFeJLd0Bcpanjk5VIkLMv2rw;jAha5928`89;~xqiI~f(Xa7 zTZ4dT&;fe{JZqp6G-o7o2<%Kx$UPi64Hrd_A=2D{H1P2{NN4A~YI*W|zq?++D`jn9 zyP5Yighil+lIHQ(_}^!tO2C9AB(N|uS^Y;pMP#p6{Iw?} zV~{VA1~3Dt6mYT)TA`4TKRJm5K072>hNgywyY%}QCiV83P%KT`&l2$HczAfg76H=) zXz#IN_Z7FuG-25vkO@Obx4u-R4pK%?^uo1_kV|tNia7>yj}z_)+VRwRxYJx*&)2D- zSYVJwqaZ9nS{iL6kIR7gfkMf}(x%nz*>wM|L5W)}ec*8dVH)x3$|iX}=K+RXmb7HZ ze?0=U0@NnNvFH`f2FZVN+xlXvhujc|i?YyS?o}kjX*sg>m|Um!%F*trA@-1rIri=d zoDE{{(@@&rnE&~4C=osc@_GNyC*Af7A#N-}CM~C=WpN+-OZ>`|iiSo4<;AA}>|%xC z1aGO5%f!WfQ;&wI5Ios*a_hco)MVI_T8{#W#aTp)h4Kh954r*v5%Bbqzn32u60*B` zfIVamctF#~GLd($0|3ue^W9!LJAxE3ZxGoiqUPB((I`ad zQ%eF&bg93JgSam!3@!)e7b3LounK6PtLhx4DXW;7t zmpUB?g^K`{yMkKl>Ow-7qONO2;c|Mj_`aFK6c|~$p+Yc7kyEk^7uN@9>&BEtRrlG>~ zrOQDo7h}M(4Atwu#a4*gil>CMaN1rwljK=av!3CC#1 z;OTtUuWD#$0IZ1MT)44ikeu8N58@y|YKImrVv($yuSC5)>N?~(FKcn0msf?-!aOu8 zT_9GXR5`LUD3ruE4s1cAx28XHGVH4tV-nhY|4l?uw_`8LxhWM`5Tn9F<9CW@Jn+Wnf_7d)MAhte$uXgA5^y&_(-MVxfnEf-cF=&p$87TQ>}jL9Jv9o5qR5LOPuJW?+I$hQVGr zUMovW5FC#c*EIMZO#s=qA@mO`G7YcPgxU&(xF5w^je~XXIt+H$m1pw6Fk>T-{ioPd z65|KV6=Uz0xg^Uh8^UJfpB7rC(P!p0$;@{<3c_1Ii=q8I zhh5XP(pz&YGXJMD(TF2eLmCkUz^LLoH#Y|o)qQABpn|JksR8?#nVDHxX({*ycHm(a z7Ut$@4(c%?oNR3HET<%xQU@L$6Ek29r^Mdl2dB*Fmkso;QD~>+`SV)tQ30#?0_Z_3 zn0T#gjc5|E(_i-n+0^#gT_Omc7rTd2!z3R*Pp=dx~1-KLH~n? z-PM~F$dlzzHU8}%Rt(_dLSWDbObyKM4~5z+nl8ab^E&IP1(@Out za8$TJ-*Q@9YYYbc3mB1qX*K{0-Ru4I6Oa&Or%*JS{4fE4nag($Olp#oZ{oa;2(9!*9}Y)Ft`>`nd@1 zKX9TIA3TEzCu%y0zCY0FTQEHmphp8 z8)kV2{tyKv2;4ouCQidegJ!5zd!m@42Cxc?&(En}Q4t zyBm6~E|Z{|F2=*t*xnnro}8yDP7vQ|37SN=MFVB0ya) z*T~PEkgLOYYA+9lKB?Aw=`-#2@&|Yey;Oms*lqWbQ>F%xImO|LDR9@*%{ZDe;o(U@ zTtd#8fv!P?KyIF)7-ePfW!K zr0L?u|JT@;$3xk+VNXf>^0rV&o>mo6){6zHY?Xa$5u!qA6DoVwX_M?4OWD$9 zU$YO16pBGvvyAzUdqzDy&-;Gg@0)*$88dTV_jO(8aUSP!9@n!MnXIbbV|eIb+UNz# zgb;Uy5=f>M)Ppn;J(^62H+N1J`mWVv!q+NN)>(`#R#x6W108|a3SDKpIf>cfs2B3P zq!)d==+9i?-Cl&%Gu9}uW4WQGjf#;5zUA@afK4F7-^+C#`RH@&R{!9j4X9NzIrP%d zzZtpa_M`d+W@cvk`r+|H>`bNWe6#jFb9tdO>htqI+Mvc0)}y{N%U+jH@PqYielW9T zC#Ui-F^NtGU>_iE=&v1NI*GlhR60!~ou0&{kao9llD4~@ot!~5m$4TlHJGcVKi`nF z@)PSlUeF5&kW8fr8R!5?K8l!&v<5f5a9|(Pj@- z;yJz5K$(u4TPNmhSH??_2h5=@!cTY=Y0v;~#eQGXrC>3Q-?d~)X&S$H#0ST=1D{=c2O=mIw$@E z^tuo(cA}Q}6=j+qN!X*_$={zINsKNFi<{(DJ^46uaAf2T!NN`Z6%D*fH0?N0Z}#~) zi+u-4VsmkJ4v6`2y|u^Tza)cO6#z{;Nm?qwDPysZ;T(hyuV3v1~ZLX*Ws2> zYUR)*rmGOVOk=6NWi_|7z+q`yu9F;PWlyv12Ue>vpFW+j(c6k<3IQadg*iO^!qoX$oc^>vKq?e33FIE1UX6&*e55Ss&V@8r^%I?Qr|tQv+JP4~5pwi#|KdSY zu>Zb=r{(6&yt1=vcSxkaXsG_{m}N;lqbs=&K<=6Awt9QUf}v$K=JvO<)gj{sIgesK z%1w_RJ_G>#Dt|dBfbmmPj8F;>67$huWkaj zc6|j!MfBo)d;Y=9AJy`gcFi&l?P!4Os8*M^eYr!g2sPP&elVi}S8U=y`dFQecQT5z z?MPkdTSt|;qpGc4i_48tTyu3~*J8OdCO?6MA< zbRF}>2kQD-=V=OsA3OE%_z0PKDfI5$WNd~q=Qszau$=A2aZgwt*|fYgxby;10dX$M zX6p(N7=+{rnWH)7l1|Qwr?=I5%99~P15XMd5F8W)Ub*TACl?4F^!SNv-t4zs8eQ?A zJwHDDu_QA{#2{7<6_5}~>!4*O8)Z+ty9o(4H9o#{&2_yyd4SMfUS8{8p-VwQL4h?K zgl=!VH|rGQD67sX*%9Wyf1TdJxl}19rxC*Mv3N;NB>WF4sk!r?)Jh#-4~h5VJy4tQ zQd6*(J$<{UGQ>!p)A_=M5}{-&l?X)0C8ZW!+mTRbwWY0ajQ`>ut6ixXp4XeLBSvCO zTHn0c%Px3O1?b^AaNKx{a&ycg%wX8<8Se^vZvp4JYMuv;;jbrLbfFmkJa6;feft81 z_}U;Gg23k4QJwrUlm=>RYvpV8-=m+a50&Vs4@pMyVY)>&xAWF>Zfb{gCo?m%>$!fM zl4ejsMTqzT^b_WFP^nac-`I3e6N1xDc+o}S^A|C(*VR^7LG~=&Ro1!@#zpusa8o^1%O)b-;s(+3fcQ`PkAL+1_!OpoCV9Z4xZS? z{7WAhXFQ)g*l(x_l|O+DMjt}*$3%~lXJ#EstbUBxanPr19|LL-6-DjqbZCBHYj`Ih zO0kRI()-ZXN~=3-$D^nXdd^3md)$}Io%EH5Xw1gO1~>~;ms3xpsWhi$nvV6<9PNHk zrY1o*?9Nut=$)1NlU|TR2+rzFq65Jl18yqsxt=Q)uhs2wl}CU6`0ThG6FJBEEppRaUS2~^wonEFF+LX)n?$=}pfZp8PS@rNvmBk0w9s>d zUo`-IOG{NzJic;edN+?rG7M^@+N*pfvOroB)k&M06C?aZuH>Hg#GQDb(W@IrT+G@7 zlfXWHw~z+fteSg(948PySWm39vmGF71JCN|g-nXDuV(3zB`AVu-bi$xTzWc)g)t;~ zc-$BAN)Yy|Gd*5s8(x3BL?bpf7G2B8VG|0H9L*1^s(O2R?l4*_Bh%P>1(;0ht;f+c zt3cuPl47lL=e}0rQf<@Skm3Oriba$C!^4I3RCIa^M;JDB)@j&WR5u&}3WaW9T{~gy zio5{3TS`M4Bot`xiHgt+(v6>yhg@Cn*CpOKXss7^z>MfCmj1dBlgU!1}q&J z9GpLYzCL;|n>mjhIRZ-F%vD}#)Y`)0X;@ej5}y<*)kew{MG+)74d_H955S29iWA-b zK<`-7EQdl@@|Nec#|5h{lumF@h|NV?Kx`g&?$5S)JKjLk;S&JoTkB2mI<7hKh&!wA z;Bd9fesW2pteD^KNQ7}i$;bCyA5wMLBR~lW*>V26G4@x83_g6&54-UuT*o>MIc&Gn zcTQ+>m-Fy!y1>uP9pvl#Aa~w^ta_QgBda}8!uF)X_+%WlHCxx-UjcIw2|<<^jA!#m z1RBWAe@jgEXY>Ay+&@|P>kN5X<#~f?3_*h3q#AXv{O&9%9m$DdWcB+XuC+&Vv34+4 zBF=)BcNy;Kp{xjcmqD(!yhfk3{n%72B#y6tP3|_ zL1^9&%Z)BBUcS7uyF0Gq83Y4hM&UPj`ZQ@woq$Qk^r~1bU0pc32+PXqd%B@f`#(Px z0Xz|7pD&USn{k8j6x$HAH!;3$0(jo(=fk za;9FJTKfeo@%WwUx$gPhbARD1+yNppxZ_XM;{VqV6&fE{`BS(KNy6}t`$(D^+!a}* z-}jA#OCk6SZv$iIvWCP6|M+)1o;M6;)pZ$d!G$!Q82+p>N_<0_x{$5xIL+Wr84uZR zMyz>Bk?WSkse5`x+z!ZJl4iLciMxnMIoxwl5Zt_Zv!GzKX9`7!BXs-K8pqcg4(&X& zDzR|t@9@nr8IRHW*l5g;2lR@$1TU-{KHs}{?@6hv%mQL^vMG3s<^p@N(!qmAv(G=P zvyfZ6_T($~X{cGCDH??&Wd^!@z^5d?~sM}@)=nmNJJN6Lt$>zAEWhx|$+V<-16zAR|e`)L- z;9fj>s}tcHkbUl(-=RhgAw(s<#fv|6b@6g@r<;~=7c2stC9qce3gvY{S7cLZvi4FV z=OEQYY3N9&GaS$1GHz+A-A|t}b0!Lu3}^s+ADO@JcUk55caBqjkFm~p88^3;LQ+FR zLtEPko?bYGVz1mZ^cy(EK7D!*rDIgllWJ71@@+fuj&Kgp(RuQ2zNAJrAQATei}38Q zYH~;MT>r)CJ#JYjRC?dOE$23Q(bDo8Qt#UTJe16>bJcsieCWYzV`#l|P)Gx*6A3*U z*pnT84D8%!YcND)qEO_~{Z_Cj>5Dln0?276kW(csygZU{7D6ev1@*nh!f1A}3K-gW zL*?Kn$o~8thT|%Z&mt1o+Jz$ow`Wuxu7e(dbrcH)QKd7=qu|aG6BCtpNJtDqh@(8p zhDRF31P>!>7?Sjb=&reQrvhDMU6t-AL&q*kOXpj*>>OOlql6b6-gmQZf;=7tZ%7^= zMNGT{R3jvws6?WUC2?Q{Zy7`j5-t3+omn(>YxSrPxR@RG9fi4?I`xkdnTUsV-ASFt z52$=2R+c%7VKl0)%%z(RJb1ip;Lcx;Un}>-pwlnK;g&zoy3&asciiHSaoBy#j*&ZO zlBJp4bSQyQ6)S)7=XZ$f;Bp@@bdKKe-k0tK%Ld#!)Lv)Jngzq)k{@rapY80gv>UE& zJn{@a(=pp}8`my^pughnLiXx2rnh-7VTb)V`tl`9H1+jucI94O8CK6wR#8Ef+Wg393hG6%#xe@dY$rel#->?S_Q%q_guVtRs$UXgmnyvei+(SzKN|+~0pT zn}=M_5Dx7u^hNIcltM8=lLku3d-hz;Ts?HU=(>&J2yvs>8~IkpGm6lj`kGK-V60@J z_~;A_k->-6E)qlw%B|bCgU&F4S3o#QxK5mplH0Mv84NU}Idv@~S)e=j&kp0M_%YMM zR+?33RAfvKYj(8i(FLfg-Fnhcw5$oB5m_Idj1}cUJN4%39H<_gO--Xr%9qgjrm>IR`7#H?o#Vz$?sI!DMR6Tkjoa#Uha5e?;`H zj^LBnG{CEdYg12DYNiMfOBtc|6@6O91$fbCg>?D;}n<9An?PlNE~_)ZU(w3ZJU0>8I>bIg%!w7 z`g(d2I-bD@4o67*JKV$Jza2`w`3o27w6%a7zv+fh>Q0%$Je)R>IyEKk;0b1WT>d#L zHRd02JjwEqP_gN&S*y;w|9Xy%b!<)LwUv~VARBpa@#Xa2G@nsi+5d?3NsCV>5p8O} zkzT7XOI*Y;70Qk2(@E@Hoe1=7)UefY8lD|fzdxEHK+FVUe*Fy&*o23DWA&|-11{yQ z%LIQuu`==6&THZE`&J6jD&B8>6lm3`HEhkVv;wz@b*IuA=}u#Vme5SZ#%}f+D+v}w zj;O1pC1KbYb8KaCMa3O2uL0DuoSdL0!n&~xqU$zs+LpH@Up{QT^dV?DFgzKbZ|^j8 z`4$CMcz?1a5vow6K2LZBHDH_dIy zJ~g36W`%`?E4CWDhlL$OKM?qUvi$D8_-5nue-SVn1GTlaQH37t@ArEl#5vR$U$$Y# zDp9ChV1D$b=`w33C9<0gm%t(3C4H?~z) zcH-4#j=B_fyiTA)mB$L^;laV?;d6)SeFO^pz z{H3}TyNpJlym```W+|y@?V8{nt=-pJPS#qrocp`&3ejKIE}>RYvlucsbkXpz_-+Nz{H@SAYPw*tlv$}wK88U64c^+$phA$TfFj{ zn)MjQQn1!SxiFhb)ud8fGys~MyG9u6?!u@sSB|DWtRl6|zC*QlU{3GQ^zA2yKTEG7 zsGET3K(#Qbj2AC#K4#Oh#o+(5Zrx*;S%NJCYk_Y$y#j)iUAuO{T%kT*?@^s~!fHv$ zGz9w&r^bL>IA~{PWGnzS5fUmfTf6>@8p@vB*n42p`eJO3lGm3R@VM*qX2&p_69`xE*^=zz{7tdvN<}a# z17Mvdyg1^tej_W134cs<4Om9iqSvN6!s%nor0`BW(dfdu zOMkHUuk?t@G?qGh7f15tm>tTeKJ9nxB7>v+>-V!r*OAy+__O!8lfiZ8$cWwLbyI}? z3m!rt{AXHE5DEXcf)YDE6XgQdcfQ?*34}E5|B}oT#Z0wFnVB-Xb}`V?F1cbp#J@I= z35@Xz*jCBDoak5ayoePSWX!#6S*+QYP8O^^Xu8FnUcc z54B|4jNgBwMx?eYvLxu1>v!&KiqiwWDX%21^iVK^O@K)Lw2XpV5s)d!E|j6rU_|+{ zjiu^K24}Z$ai!xQ_itM(l92N9a@Q}u2i8q0%^v*m1LdIK;Vpd{MLB1FVJ#uJBJPMu zu1*+Z8G^VGqf3_+;|aiJbc9%EtYK3ABaZ*(JXj+9-*@j<8jgC--)Z=6rV?|G1cYs@ z1$*sx#t)D-kx!5?+Y?9@*}v%#3?aS_eyc=udeEakwJ;~0&=@JZw#@#GU2Kn3{X$d` zS>IS5RUBusMoJ|pCG`D<^;~VuSg#1VDx$9{2XH;^GSQ_pcVFCGDYpzLl*!z!S$vwMlh4yz1#i9 zjRGWGvJ9 z{Ewo-bx!ZNv2iObEA3t?bYHiW+#UUzdlA2^_kHG%*Omu0dH^t%elFqC>FO&4nHUQ>l^Gaghc#)<41h| zXa6d>Xf|gaN|~yj3{O>0%mqDOqsQR_s`hgnD&)rFUAEMi4Wl{k17h_AbmD~*WZEeJ z2eGDWyk{HA?u3h7}$BD+4l`+YIO#Wp`j#7c#E{WofKb(F~>QO ztTz@9QdZ8rAMz_-QFA1FdV2@V*mw=4kUJ~3?M(>&&xy%7Z&C1m`k)QW{1~kdTQX4G zD5fAX>~Qo6=V&mxT|2t#t*$>7mfWShhZx8-F~71uP=lU2K!XFkXD|B9*43vawebd3JyFa7rRL%>Dh@^*< z(XVe*;zw3QBpI`3Cp!;RT)lb~;x!9X`GFhkVS-XlGw4N>h<1~ikkO5l#NeS*A6`7m z&emUI15Br*mGTX(8%Te-2fFn~;)xa{Pi6N<*eYT+IZ_dWCR5fV~M}-kq9SV690QGohdqHht$*prH=L~Kpu)NO_X^j7!1>EW3*E^;f`=iv4(u56Iyw*waROd+{1O&-M@47tg~dl)d^{bZC)7f}4n}hLf_hDGlC2+b{mydgIL-INbu9+dnR8ky%*MWb71lCOQC8+5CmaBAb1j0ku z{1+&J${@Gy>fbnEd9<-u2>LXSOYnk-`^80BUW2h!1W}MN-by$X`$|E+#KzO^&r}9B zii+Z#H#4|l%t!&6&L`w3EQ;HbKrMhxKMn~&)m#pwpKV2K{5~#u2%Ou^r()6+>THeX z8kl$K){7S}Hq0gjEir$?gF4yP60m4iIh(Sd$`eqX+Pfd&Q!Y-2xv9$dw8 zcG|ON+Kt)R8rfOB_JW_Wad9arDX7r_u(H+qo^E)fSL$qnKEb`L`j!h%WkG?Uu(0

}FzZxz~AA&n0EDOuCI< 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 DYnFjH literal 0 HcmV?d00001 diff --git a/docs/arc42/revocation-service/images/verify_status.png b/docs/arc42/revocation-service/images/verify_status.png new file mode 100644 index 0000000000000000000000000000000000000000..8442500495b1d8cfe2b29afce4b7090acfd588cb GIT binary patch literal 7252 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 rr$i>ZDFD_voN`apLK MIW: "/api/credentials/issuer" with revocable=true +alt "If revocable then get issuer status list" +MIW -> RS: "/api/v1/revocations/statusEntry +alt If status list VC not created? +RS -> RS: Create StatusList credentail +RS -> MIW: Sign new status list VC +MIW -> MIW: Save status list into issuer table +RS<-- MIW: return sighed status list VC +RS -> RS: Store StatusList VC in DB +end +RS -> RS: Get Current StatusList Index for Issuer +RS -> RS: Incease statusList Index by 1 and save in DB +RS --> MIW: Return Status List +end +group "Create and Sign VC" +MIW -> MIW: Create credentail +alt "If revocable then add status list" +MIW -> MIW: Add Status List in VC +end +MIW -> MIW: Sign VC +end group +MIW --> User: Return revocable VC +end group +@enduml + +``` + +### Verify revocable credential + +![verify_revocable_vc.png](images/verify_revocable_vc.png) + +``` +@startuml +actor User as User +participant "Managed Identity Wallet" as MIW +participant "Revocation service" as RS +title Verify VC with revocation status +group "Verify VC" +User -> MIW: "/api/credentials/validation?withCredentialExpiryDate=true&withRevocation=true" +alt "If withRevocation then check issuer status list" +MIW -> RS: "/api/v1/revocations/credentials/{issueId}" +RS -> RS: Get Current StatusList Index for Issuer +RS --> MIW: Return StatusList VC +end +group "Credential Validation" +MIW -> MIW: validate status list VC +MIW -> MIW: Valaidate vc index with encoded list of status list VC +MIW -> MIW: Check Credential is not expired +MIW -> MIW: Validate Credential JsonLD +MIW -> MIW: Verify Credential Signature +end group +MIW --> User: Return Valid or Invalid + Reason +@enduml +``` + +# Deployment + +A description of the overall structure of components including how to +run and test it locally as well as on Kubernetes in the cloud is +available in the GitHub repository: + + +The INT/DEV deployment is done using Helm charts. The charts are located in the +`charts/` sub-directory of the repository. The charts are picked up by +[ArgoCD](https://argo-cd.readthedocs.io/en/stable/) and executed, resulting in +a INT/DEV deployment into the respective Kubernetes cluster. ArgoCD polls the +GitHub status continuously and executes the Helm charts when a new commit is +detected on one of the target branches, e.g. "main". A benefit of ArgoCD is that it +automatically detects variables from the Helm charts and displays them in the +ArgoCD UI. + +[ArgoCD INT](https://argo.int.demo.catena-x.net/) +[ArgoCD DEV](https://argo.dev.demo.catena-x.net/) + +For local setup, instruction will be added in README.md file + +# Guiding Concepts + +Please refer: https://www.w3.org/TR/vc-bitstring-status-list/ + +# Design Decisions + +Revocation service is developed at Cofinity-X and as per discussion with product owner of MIW cofinity-x has decided to +contribute to the eclipse tractus-x + +# Quality Requirements + +The work being done on the project has been focused on creating a base +implementation of the Managed Identity Wallet Service. The current state has +compromised on some aspects to further progress the development. The [Risks and +Technical Depts](#technical-debts) section addresses those points in greater +detail. Nevertheless we've focused on Security and Deploy ability. + +The Revocation service sticks to the following Quality Gate +requirements where relevant and applicable: + +- Documentation: Architecture +- Documentation: Administrator\'s Guide +- Documentation: Interfaces +- Documentation: Source Code +- Documentation: Development Process +- Documentation: Standardization - Interoperability and Data Sovereignty +- Compliance: GDPR +- Test Results: Deployment/Installation +- Test Results: Code Quality Analysis +- Test Results: System Integration Tests +- Security & Compliance: Penetration Tests +- Security & Compliance: Threat Modeling +- Security & Compliance: Static Application Security Testing +- Security & Compliance: Dynamic Application Security Testing +- Security & Compliance: Secret scanning +- Security & Compliance: Software Composition Analysis +- Security & Compliance: Container Scan +- Security & Compliance: Infrastructure as Code + +# Technical Debts + +## Credential sensation is not supported + +- DID document only covers varification method. No service endpoints + +## SSI Library + +- No validation for JsonWebSignature2020 with RSA key +- No Security valdition only Sercurity Assessment done, no attack vectors are tested + +# Glossary + +| **Term** | **Definition** | +|-------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| MIW | Managed Identity wallet application | +| VC | Verifiable Credential | +| VP | Verifiable Presentation | +| Wallet | Virtual placeholder for business partner which holds VCs | +| Base wallet | Wallet for Cofinity-X. CX type of VC will be issued using this wallet | +| Status list credential | [https://www.w3.org/TR/vc-status-list/#statuslist2021credential](https://www.w3.org/TR/vc-status-list/#statuslist2021credential) | +| Status list entry | [https://www.w3.org/TR/vc-status-list/#statuslist2021credential](https://www.w3.org/TR/vc-status-list/#statuslist2021credential) | +| Status list index | [https://www.w3.org/TR/vc-status-list/#statuslist2021entry](https://www.w3.org/TR/vc-status-list/#statuslist2021entry) | +| Revocation verification | [https://www.w3.org/TR/vc-status-list/#validate-algorithm](https://www.w3.org/TR/vc-status-list/#validate-algorithm) | +| Encoded list | [https://www.w3.org/TR/vc-status-list/#bitstring-encoding](https://www.w3.org/TR/vc-status-list/#bitstring-encoding) | + +# NOTICE + +This work is licensed under the [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0). + +- SPDX-License-Identifier: Apache-2.0 +- SPDX-FileCopyrightText: 2021,2023 Contributors to the Eclipse Foundation +- Source URL: https://github.com/eclipse-tractusx/managed-identity-wallet diff --git a/docs/ArchitectureWebDID.md b/docs/database/miw/ArchitectureWebDID.md similarity index 100% rename from docs/ArchitectureWebDID.md rename to docs/database/miw/ArchitectureWebDID.md diff --git a/docs/MIW_DB_Schema_v0.0.1.md b/docs/database/miw/MIW_DB_Schema_v0.0.1.md similarity index 100% rename from docs/MIW_DB_Schema_v0.0.1.md rename to docs/database/miw/MIW_DB_Schema_v0.0.1.md diff --git a/docs/database/revocation/revocation_DB_Schema_v0.0.1.md b/docs/database/revocation/revocation_DB_Schema_v0.0.1.md new file mode 100644 index 00000000..5d742c88 --- /dev/null +++ b/docs/database/revocation/revocation_DB_Schema_v0.0.1.md @@ -0,0 +1,24 @@ +# Database Schemas for intial draft + +### Status List Index + +- id: integer (Primary Key) +- issuer_bpn_status: varchar(27) +- current_index: varchar(16) +- status_list_credential_id: varchar(256) (Unique) + +### Status list credential + +- id: integer (Primary Key) +- issuer_bpn: varchar(16) (Unique) +- credential: text +- created_at: timestamp +- modified_at: timestamp + +# NOTICE + +This work is licensed under the [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0). + +- SPDX-License-Identifier: Apache-2.0 +- SPDX-FileCopyrightText: 2021,2023 Contributors to the Eclipse Foundation +- Source URL: https://github.com/eclipse-tractusx/managed-identity-wallet diff --git a/docs/security-assessment/security-assessment-23-12.md b/docs/security-assessment/security-assessment-23-12.md index 7d7d1d7f..3452f862 100644 --- a/docs/security-assessment/security-assessment-23-12.md +++ b/docs/security-assessment/security-assessment-23-12.md @@ -1,39 +1,40 @@ # Security Assessment Managed Identity Wallet (MIW) -| | | -| --- | --- | -| Contact for product | [@OSchlienz](https://github.com/github)
[@borisrizov-zf](https://github.com/borisrizov-zf) | -| Security responsible | [@pablosec](https://github.com/pablosec)
[@SSIRKC](https://github.com/SSIRKC) | -| Version number of product | 23.12 | -| Dates of assessment | 2023-11-21: Re-assessment for release 23.12 | -| Status of assessment | RE-ASSESSMENT DRAFT | +| | | +|---------------------------|-------------------------------------------------------------------------------------------------| +| Contact for product | [@OSchlienz](https://github.com/github)
[@borisrizov-zf](https://github.com/borisrizov-zf) | +| Security responsible | [@pablosec](https://github.com/pablosec)
[@SSIRKC](https://github.com/SSIRKC) | +| Version number of product | 23.12 | +| Dates of assessment | 2023-11-21: Re-assessment for release 23.12 | +| Status of assessment | RE-ASSESSMENT DRAFT | ## Product Description -The Managed Identity Wallet (MIW) service implements the Self-Sovereign-Identity (SSI) readiness by providing a wallet hosting platform including a DID resolver, service endpoints and the company wallets itself. +The Managed Identity Wallet (MIW) service implements the Self-Sovereign-Identity (SSI) readiness by providing a wallet +hosting platform including a DID resolver, service endpoints and the company wallets itself. ### Important Links -* [MIW: README.md](https://github.com/eclipse-tractusx/managed-identity-wallet/blob/main/README.md) -* [SSI: Technical Debts.md](https://github.com/eclipse-tractusx/ssi-docu/blob/main/docs/architecture/cx-3-2/6.%20Technical%20Debts/Technical%20Debts.md) – partly outdated at date of security assessment - +* [MIW: README.md](https://github.com/eclipse-tractusx/managed-identity-wallet/blob/main/README.md) +* [Revocation Service: README.md](..%2F..%2Frevocation-service%2FREADME.md) +* [SSI: Technical Debts.md](https://github.com/eclipse-tractusx/ssi-docu/blob/main/docs/architecture/cx-3-2/6.%20Technical%20Debts/Technical%20Debts.md) – + partly outdated at date of security assessment ## Existing Security Controls ℹ️ Only controls added since last security assessment (2023-06) are listed below -* Role-based access control +* Role-based access control [README.md → Manual Keycloak Configuration](https://github.com/eclipse-tractusx/managed-identity-wallet/blob/main/README.md#manual-keycloak-configuration) - * MIW provides 7 roles: - * `add_wallets` - * `view_wallets` - * `view_wallet` - * `update_wallets` - * `update_wallet` - * `manage_app` + * MIW provides 7 roles: + * `add_wallets` + * `view_wallets` + * `view_wallet` + * `update_wallets` + * `update_wallet` + * `manage_app` * Logging is implemented with levels `OFF`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE` - * Currently there is no description of the events logged for the different levels - + * Currently there is no description of the events logged for the different levels ## Architecture/Data Flow Diagram (DFD) @@ -42,23 +43,30 @@ The Managed Identity Wallet (MIW) service implements the Self-Sovereign-Identity Source: https://github.com/pablosec/managed-identity-wallet/tree/main/docs ### Changes compared to last Security Assessment -[Security Assessment 2023-06, Release 3.2](https://confluence.catena-x.net/pages/viewpage.action?pageId=90482695) (Link only available to Catena-X Consortium Members) + +[Security Assessment 2023-06, Release 3.2](https://confluence.catena-x.net/pages/viewpage.action?pageId=90482695) (Link +only available to Catena-X Consortium Members) + * No architectural changes in codebase compared to last security assessment, performed in June 2023. -* Product team currently working on STS (Secure Token Service) and Presentation Flow for VC/VP, planned for rollout in version *24.05*. +* Product team currently working on STS (Secure Token Service) and Presentation Flow for VC/VP, planned for rollout in + version *24.05*. ### Features for upcoming versions -See also [SSI: Technical Debts.md](https://github.com/eclipse-tractusx/ssi-docu/blob/main/docs/architecture/cx-3-2/6.%20Technical%20Debts/Technical%20Debts.md) +See +also [SSI: Technical Debts.md](https://github.com/eclipse-tractusx/ssi-docu/blob/main/docs/architecture/cx-3-2/6.%20Technical%20Debts/Technical%20Debts.md) -* Revocation Service and substituting Summary Credentials (→ 24.05) * Use of key rotation (→ 24.05) * Switch to actually decentralized DID documents (→ will be implemented, but not scheduled at the moment) * Create credentials outside of MIW (→ will be implemented, but not scheduled at the moment) -⚠️ Multi-tenancy will not be implemented as part of the development during the Catena-X consortium phase. Security risk associated with lack of multi-tenancy is accepted. +⚠️ Multi-tenancy will not be implemented as part of the development during the Catena-X consortium phase. Security risk +associated with lack of multi-tenancy is accepted. ## Threats & Risks + The threats and risks identified during this security assessment can be found in the following issues: + * eclipse-tractusx/managed-identity-wallet#164 * eclipse-tractusx/managed-identity-wallet#165 * eclipse-tractusx/managed-identity-wallet#86 (finding was already created as issue before assessment) diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java index 13c521ee..d9808979 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java @@ -43,17 +43,19 @@ public class RevocationApiControllerApiDocs { value = { @ApiResponse( responseCode = "200", - description = "Verifiable credential revoked successfully.", - content = @Content(examples = { @ExampleObject(description = "if credential is revoked", value = """ - { - "status":"revoked" - } - """), - @ExampleObject(description = "if credential is is active", value = """ - { - "status":"active" - } - """) })), + description = "if credential is revoked", + content = @Content( + examples = { + @ExampleObject(description = "if credential is revoked", value = """ + { + "status":"revoked" + } + """), + @ExampleObject(description = "if credential is is active", value = """ + { + "status":"active" + } + """) })), @ApiResponse( responseCode = "401", description = "UnauthorizedException: invalid token", @@ -68,9 +70,7 @@ public class RevocationApiControllerApiDocs { content = @Content( examples = - @ExampleObject( - value = - ""), + @ExampleObject(), mediaType = "application/json")), @ApiResponse( responseCode = "500", @@ -225,8 +225,41 @@ public class RevocationApiControllerApiDocs { @Content( examples = @ExampleObject( - value = - "{\"@context\": [\"https://www.w3.org/2018/credentials/v1\", \"https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json\", \"https://w3id.org/security/suites/jws-2020/v1\"], \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\", \"type\": [\"VerifiableCredential\", \"BitstringStatusListCredential\"], \"issuer\": \"did:web:localhost:BPNL000000000000\", \"issuanceDate\": \"2024-02-05T09:39:58Z\", \"credentialSubject\": [{\"statusPurpose\": \"revocation\", \"id\": \"http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1\", \"type\": \"BitstringStatusList\", \"encodedList\": \"H4sIAAAAAAAA/wMAAAAAAAAAAAA=\"}], \"proof\": {\"proofPurpose\": \"assertionMethod\", \"type\": \"JsonWebSignature2020\", \"verificationMethod\": \"did:web:localhost:BPNL000000000000#ed463e4c-b900-481a-b5d0-9ae439c434ae\", \"created\": \"2024-02-05T09:39:58Z\", \"jws\": \"eyJhbGciOiJFZERTQSJ9..swX1PLJkSlxB6JMmY4a2uUzR-uszlyLrVdNppoYSx4PTV1LzQrDb0afzp_dvTNUWEYDI57a8iPh78BDjqMjSDQ\"}}"), + value = """ + { + "@context": + [ + "https://www.w3.org/2018/credentials/v1", + "https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json", + "https://w3id.org/security/suites/jws-2020/v1" + ], + "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": + [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "issuer": "did:web:localhost:BPNL000000000000", + "issuanceDate": "2024-02-05T09:39:58Z", + "credentialSubject": + [ + { + "statusPurpose": "revocation", + "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", + "type": "BitstringStatusList", + "encodedList": "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" + } + ], + "proof": + { + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "did:web:localhost:BPNL000000000000#ed463e4c-b900-481a-b5d0-9ae439c434ae", + "created": "2024-02-05T09:39:58Z", + "jws": "eyJhbGciOiJFZERTQSJ9..swX1PLJkSlxB6JMmY4a2uUzR-uszlyLrVdNppoYSx4PTV1LzQrDb0afzp_dvTNUWEYDI57a8iPh78BDjqMjSDQ" + } + } + """), mediaType = "application/json") }), @ApiResponse( From 272e22a3153029d6401be5536fecb2193097ebf3 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 17 Jun 2024 21:00:30 +0530 Subject: [PATCH 28/60] doc: API doc updated --- docs/api/revocation-service/openapi_v001.json | 70 +++++++++++-------- .../RevocationApiControllerApiDocs.java | 19 ++--- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/docs/api/revocation-service/openapi_v001.json b/docs/api/revocation-service/openapi_v001.json index 0886659c..423ead47 100644 --- a/docs/api/revocation-service/openapi_v001.json +++ b/docs/api/revocation-service/openapi_v001.json @@ -54,23 +54,35 @@ "required" : true }, "responses" : { - "401" : { - "description" : "UnauthorizedException: invalid token" - }, "200" : { - "description" : "if credential is revoked" - }, - "500" : { - "description" : "RevocationServiceException: Internal Server Error" - }, - "409" : { - "description" : "ConflictException: Revocation service error", + "description" : "Status of credential", "content" : { - "application/json" : {} + "application/json" : { + "examples" : { + "if credential is revoked" : { + "description" : "if credential is revoked", + "value" : { + "status" : "revoked" + } + }, + "if credential is active" : { + "description" : "if credential is is active", + "value" : { + "status" : "active" + } + } + } + } } }, "403" : { "description" : "ForbiddenException: invalid caller" + }, + "401" : { + "description" : "UnauthorizedException: invalid token" + }, + "500" : { + "description" : "RevocationServiceException: Internal Server Error" } } } @@ -98,12 +110,6 @@ "required" : true }, "responses" : { - "401" : { - "description" : "UnauthorizedException: invalid token" - }, - "500" : { - "description" : "RevocationServiceException: Internal Server Error" - }, "403" : { "description" : "ForbiddenException: invalid caller" }, @@ -120,6 +126,12 @@ } } } + }, + "401" : { + "description" : "UnauthorizedException: invalid token" + }, + "500" : { + "description" : "RevocationServiceException: Internal Server Error" } } } @@ -150,15 +162,6 @@ "required" : true }, "responses" : { - "200" : { - "description" : "Verifiable credential revoked successfully." - }, - "401" : { - "description" : "UnauthorizedException: invalid token" - }, - "500" : { - "description" : "RevocationServiceException: Internal Server Error" - }, "403" : { "description" : "ForbiddenException: invalid caller" }, @@ -176,6 +179,15 @@ } } } + }, + "401" : { + "description" : "UnauthorizedException: invalid token" + }, + "200" : { + "description" : "Verifiable credential revoked successfully." + }, + "500" : { + "description" : "RevocationServiceException: Internal Server Error" } } } @@ -221,9 +233,6 @@ } ], "responses" : { - "500" : { - "description" : "RevocationServiceException: Internal Server Error" - }, "200" : { "description" : "Get Status list credential ", "content" : { @@ -262,6 +271,9 @@ }, "404" : { "description" : "Status list credential not found" + }, + "500" : { + "description" : "RevocationServiceException: Internal Server Error" } } } diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java index d9808979..0f63fc68 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java @@ -28,6 +28,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.MediaType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -43,19 +44,19 @@ public class RevocationApiControllerApiDocs { value = { @ApiResponse( responseCode = "200", - description = "if credential is revoked", - content = @Content( + description = "Status of credential", + content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = { - @ExampleObject(description = "if credential is revoked", value = """ + @ExampleObject(name = "if credential is revoked", description = "if credential is revoked", value = """ { "status":"revoked" } """), - @ExampleObject(description = "if credential is is active", value = """ + @ExampleObject(name = "if credential is active", description = "if credential is is active", value = """ { "status":"active" } - """) })), + """) }) }), @ApiResponse( responseCode = "401", description = "UnauthorizedException: invalid token", @@ -64,14 +65,6 @@ public class RevocationApiControllerApiDocs { responseCode = "403", description = "ForbiddenException: invalid caller", content = @Content()), - @ApiResponse( - responseCode = "409", - description = "ConflictException: Revocation service error", - content = - @Content( - examples = - @ExampleObject(), - mediaType = "application/json")), @ApiResponse( responseCode = "500", description = "RevocationServiceException: Internal Server Error", From 4465f24c3ce7434e70448b0e787a6abab5393f63 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 11:39:26 +0530 Subject: [PATCH 29/60] commons dao version updated --- DEPENDENCIES | 11 ++----- build.gradle | 5 ++- gradle.properties | 2 +- .../config/ApplicationConfig.java | 8 +---- .../dao/entity/MIWBaseEntity.java | 2 +- .../HoldersCredentialRepository.java | 2 +- .../IssuersCredentialRepository.java | 2 +- .../dao/repository/JtiRepository.java | 2 +- .../dao/repository/WalletKeyRepository.java | 2 +- .../dao/repository/WalletRepository.java | 2 +- .../service/HoldersCredentialService.java | 31 ++++++------------- .../service/IssuersCredentialService.java | 31 ++++++------------- .../service/PresentationService.java | 11 ++----- .../service/WalletKeyService.java | 10 ++---- .../service/WalletService.java | 19 ++++-------- .../service/IssuersCredentialServiceTest.java | 3 -- 16 files changed, 42 insertions(+), 101 deletions(-) diff --git a/DEPENDENCIES b/DEPENDENCIES index 5ef621c7..7f02ffed 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -44,10 +44,11 @@ maven/mavencentral/com.nimbusds/lang-tag/1.7, Apache-2.0, approved, clearlydefin maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.3, Apache-2.0, approved, #11701 maven/mavencentral/com.nimbusds/oauth2-oidc-sdk/9.43.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.opencsv/opencsv/5.9, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.smartsensesolutions/commons-dao/0.0.5, Apache-2.0, approved, #9176 +maven/mavencentral/com.smartsensesolutions/commons-dao/1.0.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.sun.activation/jakarta.activation/1.2.1, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/com.sun.istack/istack-commons-runtime/4.1.2, BSD-3-Clause, approved, #15290 maven/mavencentral/com.sun.mail/jakarta.mail/1.6.5, EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, ee4j.mail +maven/mavencentral/com.teketik/mock-in-bean/boot2-v1.5.2, Apache-2.0, approved, clearlydefined maven/mavencentral/com.vaadin.external.google/android-json/0.0.20131108.vaadin1, Apache-2.0, approved, CQ21310 maven/mavencentral/com.zaxxer/HikariCP/5.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/commons-beanutils/commons-beanutils/1.9.4, Apache-2.0, approved, CQ12654 @@ -132,13 +133,9 @@ maven/mavencentral/org.hdrhistogram/HdrHistogram/2.2.2, BSD-2-Clause AND CC0-1.0 maven/mavencentral/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final, LGPL-2.1-only, approved, #6962 maven/mavencentral/org.hibernate.orm/hibernate-core/6.5.2.Final, LGPL-2.1-only AND (EPL-2.0 OR BSD-3-Clause) AND LGPL-2.1-or-later AND MIT, approved, #15118 maven/mavencentral/org.hibernate.validator/hibernate-validator/8.0.1.Final, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.8, EPL-2.0, approved, CQ23285 maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.9, EPL-2.0, approved, CQ23285 -maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.8, EPL-2.0, approved, #1068 maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.9, EPL-2.0, approved, #1068 -maven/mavencentral/org.jacoco/org.jacoco.core/0.8.8, EPL-2.0, approved, CQ23283 maven/mavencentral/org.jacoco/org.jacoco.core/0.8.9, EPL-2.0, approved, CQ23283 -maven/mavencentral/org.jacoco/org.jacoco.report/0.8.8, EPL-2.0 AND Apache-2.0, approved, CQ23284 maven/mavencentral/org.jacoco/org.jacoco.report/0.8.9, EPL-2.0 AND Apache-2.0, approved, CQ23284 maven/mavencentral/org.jboss.logging/jboss-logging/3.5.3.Final, Apache-2.0, approved, #9471 maven/mavencentral/org.jboss.resteasy/resteasy-client-api/4.7.7.Final, Apache-2.0, approved, clearlydefined @@ -173,12 +170,8 @@ maven/mavencentral/org.mockito/mockito-inline/5.2.0, MIT, approved, clearlydefin maven/mavencentral/org.mockito/mockito-junit-jupiter/5.11.0, MIT, approved, #13504 maven/mavencentral/org.objenesis/objenesis/3.3, Apache-2.0, approved, clearlydefined maven/mavencentral/org.opentest4j/opentest4j/1.3.0, Apache-2.0, approved, #9713 -maven/mavencentral/org.ow2.asm/asm-analysis/9.2, BSD-3-Clause, approved, clearlydefined -maven/mavencentral/org.ow2.asm/asm-commons/9.2, BSD-3-Clause, approved, clearlydefined maven/mavencentral/org.ow2.asm/asm-commons/9.5, BSD-3-Clause, approved, #7553 -maven/mavencentral/org.ow2.asm/asm-tree/9.2, BSD-3-Clause, approved, clearlydefined maven/mavencentral/org.ow2.asm/asm-tree/9.5, BSD-3-Clause, approved, #7555 -maven/mavencentral/org.ow2.asm/asm/9.2, BSD-3-Clause, approved, CQ23635 maven/mavencentral/org.ow2.asm/asm/9.5, BSD-3-Clause, approved, #7554 maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 maven/mavencentral/org.postgresql/postgresql/42.7.3, BSD-2-Clause AND Apache-2.0, approved, #11681 diff --git a/build.gradle b/build.gradle index dfa23d53..4dabeada 100644 --- a/build.gradle +++ b/build.gradle @@ -189,7 +189,10 @@ tasks.register('dashDependencies') { dashDependencies -> def finalDeps = [] for (final def d in deps) { //skip main module dependencies - if (d.toString() == "project :miw" || d.toString() == "project :revocation-service") { + if (d.toString() == "project :miw" + || d.toString() == "project :revocation-service" + || d.toString() == "project :wallet-commons" + ) { println(" - " + d.toString() + " -") } else { diff --git a/gradle.properties b/gradle.properties index 9b250fba..4e9e6e54 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,6 @@ lombokVersion=1.18.32 gsonVersion=2.10.1 ssiLibVersion=0.0.19 wiremockVersion=3.4.2 -commonsDaoVersion=0.0.5 +commonsDaoVersion=1.0.1 appGroup=org.eclipse.tractusx.managedidentitywallets mockInBeanVersion=boot2-v1.5.2 diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java index 9cefda80..22ac481f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; import org.eclipse.tractusx.managedidentitywallets.domain.SigningServiceType; @@ -81,11 +80,6 @@ public ObjectMapper objectMapper() { return objectMapper; } - @Bean - public SpecificationUtil specificationUtil() { - return new SpecificationUtil<>(); - } - @Override public void addViewControllers(ViewControllerRegistry registry) { String redirectUri = properties.getPath(); @@ -117,7 +111,7 @@ public Map availableKeyStorages(List available = new EnumMap<>(SigningServiceType.class); storages.forEach( s -> { - if(s instanceof LocalSigningService local){ + if (s instanceof LocalSigningService local) { local.setKeyProvider(localSigningKeyProvider); } available.put(s.getSupportedServiceType(), s); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/MIWBaseEntity.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/MIWBaseEntity.java index 2a8a5864..850bda66 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/MIWBaseEntity.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/entity/MIWBaseEntity.java @@ -22,7 +22,7 @@ package org.eclipse.tractusx.managedidentitywallets.dao.entity; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.smartsensesolutions.java.commons.base.entity.BaseEntity; +import com.smartsensesolutions.commons.dao.base.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.Temporal; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java index ee462db3..8e9c9702 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/HoldersCredentialRepository.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.dao.repository; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseRepository; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.springframework.data.jpa.repository.Query; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/IssuersCredentialRepository.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/IssuersCredentialRepository.java index 8b512b25..92b30788 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/IssuersCredentialRepository.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/IssuersCredentialRepository.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.dao.repository; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseRepository; import org.eclipse.tractusx.managedidentitywallets.dao.entity.IssuersCredential; import java.util.List; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/JtiRepository.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/JtiRepository.java index 69c83a4f..0079fee1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/JtiRepository.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/JtiRepository.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.dao.repository; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseRepository; import org.eclipse.tractusx.managedidentitywallets.dao.entity.JtiRecord; import org.springframework.stereotype.Repository; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletKeyRepository.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletKeyRepository.java index 54196d73..4c403bf0 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletKeyRepository.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletKeyRepository.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.dao.repository; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseRepository; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; import org.springframework.stereotype.Repository; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletRepository.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletRepository.java index 4020d004..d23529e6 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletRepository.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/dao/repository/WalletRepository.java @@ -21,7 +21,7 @@ package org.eclipse.tractusx.managedidentitywallets.dao.repository; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseRepository; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.springframework.stereotype.Repository; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java index 4eae8b25..4d53a952 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java @@ -21,14 +21,12 @@ package org.eclipse.tractusx.managedidentitywallets.service; -import com.smartsensesolutions.java.commons.FilterRequest; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; -import com.smartsensesolutions.java.commons.base.service.BaseService; -import com.smartsensesolutions.java.commons.criteria.CriteriaOperator; -import com.smartsensesolutions.java.commons.operator.Operator; -import com.smartsensesolutions.java.commons.sort.Sort; -import com.smartsensesolutions.java.commons.sort.SortType; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; +import com.smartsensesolutions.commons.dao.base.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseService; +import com.smartsensesolutions.commons.dao.filter.FilterRequest; +import com.smartsensesolutions.commons.dao.filter.sort.Sort; +import com.smartsensesolutions.commons.dao.filter.sort.SortType; +import com.smartsensesolutions.commons.dao.operator.Operator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; @@ -78,8 +76,6 @@ public class HoldersCredentialService extends BaseService credentialSpecificationUtil; - private final Map availableSigningServices; private final RevocationService revocationService; @@ -90,11 +86,6 @@ protected BaseRepository getRepository() { return holdersCredentialRepository; } - @Override - protected SpecificationUtil getSpecificationUtil() { - return credentialSpecificationUtil; - } - /** * Gets credentials. @@ -119,21 +110,17 @@ public PageImpl getCredentials(GetCredentialsCommand comman if (StringUtils.hasText(command.getCredentialId())) { filterRequest.appendCriteria(StringPool.CREDENTIAL_ID, Operator.EQUALS, command.getCredentialId()); } - FilterRequest request = new FilterRequest(); if (!CollectionUtils.isEmpty(command.getType())) { - request.setPage(filterRequest.getPage()); - request.setSize(filterRequest.getSize()); - request.setCriteriaOperator(CriteriaOperator.OR); for (String str : command.getType()) { - request.appendCriteria(StringPool.TYPE, Operator.CONTAIN, str); + filterRequest.appendOrCriteria(StringPool.TYPE, Operator.CONTAIN, str); } } Sort sort = new Sort(); sort.setColumn(command.getSortColumn()); sort.setSortType(SortType.valueOf(command.getSortType().toUpperCase())); - filterRequest.setSort(sort); - Page filter = filter(filterRequest, request, CriteriaOperator.AND); + filterRequest.setSort(List.of(sort)); + Page filter = filter(filterRequest); List list = new ArrayList<>(filter.getContent().size()); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java index 6f1592dc..fbdf58c6 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java @@ -23,14 +23,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jwt.SignedJWT; -import com.smartsensesolutions.java.commons.FilterRequest; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; -import com.smartsensesolutions.java.commons.base.service.BaseService; -import com.smartsensesolutions.java.commons.criteria.CriteriaOperator; -import com.smartsensesolutions.java.commons.operator.Operator; -import com.smartsensesolutions.java.commons.sort.Sort; -import com.smartsensesolutions.java.commons.sort.SortType; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; +import com.smartsensesolutions.commons.dao.base.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseService; +import com.smartsensesolutions.commons.dao.filter.FilterRequest; +import com.smartsensesolutions.commons.dao.filter.sort.Sort; +import com.smartsensesolutions.commons.dao.filter.sort.SortType; +import com.smartsensesolutions.commons.dao.operator.Operator; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -105,8 +103,6 @@ public class IssuersCredentialService extends BaseService credentialSpecificationUtil; - private final HoldersCredentialRepository holdersCredentialRepository; private final CommonService commonService; @@ -125,11 +121,6 @@ protected BaseRepository getRepository() { return issuersCredentialRepository; } - @Override - protected SpecificationUtil getSpecificationUtil() { - return credentialSpecificationUtil; - } - /** * Gets credentials. @@ -154,21 +145,17 @@ public PageImpl getCredentials(GetCredentialsCommand comman if (StringUtils.hasText(command.getCredentialId())) { filterRequest.appendCriteria(StringPool.CREDENTIAL_ID, Operator.EQUALS, command.getCredentialId()); } - FilterRequest request = new FilterRequest(); if (!CollectionUtils.isEmpty(command.getType())) { - request.setPage(filterRequest.getPage()); - request.setSize(filterRequest.getSize()); - request.setCriteriaOperator(CriteriaOperator.OR); for (String str : command.getType()) { - request.appendCriteria(StringPool.TYPE, Operator.CONTAIN, str); + filterRequest.appendOrCriteria(StringPool.TYPE, Operator.CONTAIN, str); } } Sort sort = new Sort(); sort.setColumn(command.getSortColumn()); sort.setSortType(SortType.valueOf(command.getSortType().toUpperCase())); - filterRequest.setSort(sort); - Page filter = filter(filterRequest, request, CriteriaOperator.AND); + filterRequest.setSort(List.of(sort)); + Page filter = super.filter(filterRequest); List list = new ArrayList<>(filter.getContent().size()); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java index e5c1b169..5eae9e44 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java @@ -24,9 +24,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; -import com.smartsensesolutions.java.commons.base.service.BaseService; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; +import com.smartsensesolutions.commons.dao.base.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseService; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -85,8 +84,6 @@ public class PresentationService extends BaseService { private final HoldersCredentialRepository holdersCredentialRepository; - private final SpecificationUtil credentialSpecificationUtil; - private final CommonService commonService; @@ -105,10 +102,6 @@ protected BaseRepository getRepository() { return holdersCredentialRepository; } - @Override - protected SpecificationUtil getSpecificationUtil() { - return credentialSpecificationUtil; - } /** * Create presentation map. diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java index 548a8bfe..5f7f7dde 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletKeyService.java @@ -21,9 +21,8 @@ package org.eclipse.tractusx.managedidentitywallets.service; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; -import com.smartsensesolutions.java.commons.base.service.BaseService; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; +import com.smartsensesolutions.commons.dao.base.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseService; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.bouncycastle.util.io.pem.PemReader; @@ -55,7 +54,6 @@ public class WalletKeyService extends BaseService { private final WalletKeyRepository walletKeyRepository; - private final SpecificationUtil specificationUtil; private final EncryptionUtils encryptionUtils; @@ -64,10 +62,6 @@ public BaseRepository getRepository() { return walletKeyRepository; } - @Override - protected SpecificationUtil getSpecificationUtil() { - return specificationUtil; - } /** * Get private key by wallet identifier as bytes byte [ ]. diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java index 76237e05..20147370 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/WalletService.java @@ -25,12 +25,11 @@ import com.nimbusds.jose.jwk.ECKey; import com.nimbusds.jose.jwk.KeyType; import com.nimbusds.jose.jwk.KeyUse; -import com.smartsensesolutions.java.commons.FilterRequest; -import com.smartsensesolutions.java.commons.base.repository.BaseRepository; -import com.smartsensesolutions.java.commons.base.service.BaseService; -import com.smartsensesolutions.java.commons.sort.Sort; -import com.smartsensesolutions.java.commons.sort.SortType; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; +import com.smartsensesolutions.commons.dao.base.BaseRepository; +import com.smartsensesolutions.commons.dao.base.BaseService; +import com.smartsensesolutions.commons.dao.filter.FilterRequest; +import com.smartsensesolutions.commons.dao.filter.sort.Sort; +import com.smartsensesolutions.commons.dao.filter.sort.SortType; import jakarta.annotation.PostConstruct; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -99,8 +98,6 @@ public class WalletService extends BaseService { private final HoldersCredentialRepository holdersCredentialRepository; - private final SpecificationUtil walletSpecificationUtil; - private final CommonService commonService; private final Map availableSigningServices; @@ -116,10 +113,6 @@ protected BaseRepository getRepository() { return walletRepository; } - @Override - protected SpecificationUtil getSpecificationUtil() { - return walletSpecificationUtil; - } /** * Store credential map. @@ -201,7 +194,7 @@ public Page getWallets(int pageNumber, int size, String sortColumn, Stri Sort sort = new Sort(); sort.setColumn(sortColumn); sort.setSortType(SortType.valueOf(sortType.toUpperCase())); - filterRequest.setSort(sort); + filterRequest.setSort(List.of(sort)); return filter(filterRequest); } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java index 4127dc59..056e9a49 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialServiceTest.java @@ -25,7 +25,6 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.nimbusds.jose.JWSObject; import com.nimbusds.jwt.SignedJWT; -import com.smartsensesolutions.java.commons.specification.SpecificationUtil; import lombok.SneakyThrows; import org.apache.commons.lang3.time.DateUtils; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; @@ -33,7 +32,6 @@ import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; -import org.eclipse.tractusx.managedidentitywallets.dao.entity.IssuersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.entity.WalletKey; import org.eclipse.tractusx.managedidentitywallets.dao.repository.HoldersCredentialRepository; @@ -152,7 +150,6 @@ public static void beforeAll() throws SQLException { issuersCredentialService = new IssuersCredentialService( issuersCredentialRepository, miwSettings, - new SpecificationUtil(), holdersCredentialRepository, commonService, objectMapper, revocationService, revocationSettings); } From 4ebed6f93a44217e95ec213d58a5b898d99a36b5 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 11:49:52 +0530 Subject: [PATCH 30/60] test: check if VC is has credential status --- .../service/HoldersCredentialService.java | 12 ++++++++++++ .../vc/HoldersCredentialTest.java | 2 ++ .../vc/IssuersCredentialTest.java | 1 + 3 files changed, 15 insertions(+) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java index 4d53a952..db21186e 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java @@ -35,6 +35,7 @@ import org.eclipse.tractusx.managedidentitywallets.commons.constant.SupportedAlgorithms; import org.eclipse.tractusx.managedidentitywallets.commons.exception.ForbiddenException; import org.eclipse.tractusx.managedidentitywallets.commons.utils.Validate; +import org.eclipse.tractusx.managedidentitywallets.config.RevocationSettings; import org.eclipse.tractusx.managedidentitywallets.dao.entity.HoldersCredential; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dao.repository.HoldersCredentialRepository; @@ -54,6 +55,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import java.net.URI; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -80,6 +82,8 @@ public class HoldersCredentialService extends BaseService getRepository() { @@ -192,6 +196,14 @@ public CredentialsResponse issueCredential(Map data, String call //get credential status in case of revocation VerifiableCredentialStatusList2021Entry statusListEntry = revocationService.getStatusListEntry(issuerWallet.getBpn(), token); builder.verifiableCredentialStatus(statusListEntry); + + //add revocation context if missing + List uris = verifiableCredential.getContext(); + if (!uris.contains(revocationSettings.bitStringStatusListContext())) { + uris.add(revocationSettings.bitStringStatusListContext()); + builder.contexts(uris); + } + } CredentialCreationConfig holdersCredentialCreationConfig = builder.build(); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java index 16efe6b5..0eea9a1d 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/HoldersCredentialTest.java @@ -150,6 +150,8 @@ void issueCredentialTest200() throws JsonProcessingException { Assertions.assertEquals(HttpStatus.CREATED.value(), response.getStatusCode().value()); VerifiableCredential verifiableCredential = new VerifiableCredential(new ObjectMapper().readValue(response.getBody(), Map.class)); Assertions.assertNotNull(verifiableCredential.getProof()); + Assertions.assertNotNull(verifiableCredential.getVerifiableCredentialStatus()); + List credentials = holdersCredentialRepository.getByHolderDidAndType(did, type); Assertions.assertFalse(credentials.isEmpty()); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java index 52f73c4d..ea8074e4 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/vc/IssuersCredentialTest.java @@ -237,6 +237,7 @@ void issueCredentialsToBaseWallet200() throws JsonProcessingException { VerifiableCredential verifiableCredential = TestUtils.issueCustomVCUsingBaseWallet(baseBpn, miwSettings.authorityWalletDid(), miwSettings.authorityWalletDid(), type, headers, miwSettings, objectMapper, restTemplate); Assertions.assertNotNull(verifiableCredential.getProof()); + Assertions.assertNotNull(verifiableCredential.getVerifiableCredentialStatus()); List credentials = holdersCredentialRepository.getByHolderDidAndType(miwSettings.authorityWalletDid(), type); Assertions.assertFalse(credentials.isEmpty()); From 60e3a5ccf7a77a6efc6428650d9f3091e6002707 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 16:31:14 +0530 Subject: [PATCH 31/60] fix: dependencies addded at individual project level --- build.gradle | 105 +++++++++-------- DEPENDENCIES => miw/DEPENDENCIES | 18 --- revocation-service/DEPENDENCIES | 194 +++++++++++++++++++++++++++++++ wallet-commons/DEPENDENCIES | 41 +++++++ 4 files changed, 287 insertions(+), 71 deletions(-) rename DEPENDENCIES => miw/DEPENDENCIES (92%) create mode 100644 revocation-service/DEPENDENCIES create mode 100644 wallet-commons/DEPENDENCIES diff --git a/build.gradle b/build.gradle index 4dabeada..5a7acd2f 100644 --- a/build.gradle +++ b/build.gradle @@ -144,34 +144,32 @@ subprojects { } check.dependsOn jacocoTestCoverageVerification -} -tasks.register('dashDownload', Download) { - description = 'Download the Dash License Tool standalone jar' - group = 'License' - src 'https://repo.eclipse.org/service/local/artifact/maven/redirect?r=dash-licenses&g=org.eclipse.dash&a=org.eclipse.dash.licenses&v=LATEST' - dest rootProject.file('dash.jar') - overwrite false -} + tasks.register('dashDownload', Download) { + description = 'Download the Dash License Tool standalone jar' + group = 'License' + src 'https://repo.eclipse.org/service/local/artifact/maven/redirect?r=dash-licenses&g=org.eclipse.dash&a=org.eclipse.dash.licenses&v=LATEST' + dest rootProject.file('dash.jar') + overwrite false + } // This task is primarily used by CIs -tasks.register('dashClean') { - description = "Clean all files used by the 'License' group" - group = 'License' - logger.lifecycle("Removing 'dash.jar'") - rootProject.file('dash.jar').delete() - logger.lifecycle("Removing 'deps.txt'") - file('deps.txt').delete() -} + tasks.register('dashClean') { + description = "Clean all files used by the 'License' group" + group = 'License' + logger.lifecycle("Removing 'dash.jar'") + rootProject.file('dash.jar').delete() + logger.lifecycle("Removing 'deps.txt'") + file('deps.txt').delete() + } -tasks.register('dashDependencies') { dashDependencies -> - description = "Output all project dependencies as a flat list and save an intermediate file 'deps.txt'." - group = 'License' - dashDependencies.dependsOn('dashDownload') - doLast { - def deps = [] - project.allprojects.each { project -> + tasks.register('dashDependencies') { dashDependencies -> + description = "Output all project dependencies as a flat list and save an intermediate file 'deps.txt'." + group = 'License' + dashDependencies.dependsOn('dashDownload') + doLast { + def deps = [] project.configurations.each { conf -> // resolving 'archives' or 'default' is deprecated if (conf.canBeResolved && conf.getName() != 'archives' && conf.getName() != 'default') { @@ -184,40 +182,41 @@ tasks.register('dashDependencies') { dashDependencies -> ) } } - } - def finalDeps = [] - for (final def d in deps) { - //skip main module dependencies - if (d.toString() == "project :miw" - || d.toString() == "project :revocation-service" - || d.toString() == "project :wallet-commons" - ) { - println(" - " + d.toString() + " -") - - } else { - finalDeps.add(d) + def finalDeps = [] + for (final def d in deps) { + //skip main module dependencies + if (d.toString() == "project :miw" + || d.toString() == "project :revocation-service" + || d.toString() == "project :wallet-commons" + ) { + println(" - " + d.toString() + " -") + + } else { + finalDeps.add(d) + } } - } - def uniqueSorted = finalDeps.unique().sort() - uniqueSorted.each { logger.quiet("{}", it) } - file("deps.txt").write(uniqueSorted.join('\n')) + def uniqueSorted = finalDeps.unique().sort() + uniqueSorted.each { logger.quiet("{}", it) } + file("deps.txt").write(uniqueSorted.join('\n')) + } } -} -tasks.register('dashLicenseCheck', JavaExec) { dashLicenseCheck -> - description = "Run the Dash License Tool and save the summary in the 'DEPENDENCIES' file" - group = 'License' - dashLicenseCheck.dependsOn('dashDownload') - dashLicenseCheck.dependsOn('dashDependencies') - doFirst { - classpath = rootProject.files('dash.jar') - // docs: https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-04 - args('-project', 'automotive.tractusx', '-summary', 'DEPENDENCIES', 'deps.txt') - } - doLast { - logger.lifecycle("Removing 'deps.txt' now.") - file('deps.txt').delete() + tasks.register('dashLicenseCheck', JavaExec) { dashLicenseCheck -> + description = "Run the Dash License Tool and save the summary in the 'DEPENDENCIES' file" + group = 'License' + dashLicenseCheck.dependsOn('dashDownload') + dashLicenseCheck.dependsOn('dashDependencies') + doFirst { + classpath = rootProject.files('dash.jar') + // docs: https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-04 + args('-project', 'automotive.tractusx', '-summary', 'DEPENDENCIES', 'deps.txt') + } + doLast { + logger.lifecycle("Removing 'deps.txt' now.") + file('deps.txt').delete() + } } + } diff --git a/DEPENDENCIES b/miw/DEPENDENCIES similarity index 92% rename from DEPENDENCIES rename to miw/DEPENDENCIES index 7f02ffed..9f92211f 100644 --- a/DEPENDENCIES +++ b/miw/DEPENDENCIES @@ -19,11 +19,8 @@ maven/mavencentral/com.github.ben-manes.caffeine/caffeine/3.1.8, Apache-2.0, app maven/mavencentral/com.github.curious-odd-man/rgxgen/1.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.dasniko/testcontainers-keycloak/2.5.0, Apache-2.0, approved, #9175 maven/mavencentral/com.github.docker-java/docker-java-api/3.3.4, Apache-2.0, approved, #10346 -maven/mavencentral/com.github.docker-java/docker-java-api/3.3.6, Apache-2.0, approved, #10346 maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.4, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 -maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.6, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.4, Apache-2.0, approved, #7942 -maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.6, Apache-2.0, approved, #7942 maven/mavencentral/com.github.java-json-tools/btf/1.3, Apache-2.0 OR LGPL-3.0-only, approved, #15201 maven/mavencentral/com.github.java-json-tools/jackson-coreutils/2.0, Apache-2.0 OR LGPL-3.0-or-later, approved, #15186 maven/mavencentral/com.github.java-json-tools/json-patch/1.13, Apache-2.0 OR LGPL-3.0-or-later, approved, CQ23929 @@ -36,13 +33,9 @@ maven/mavencentral/com.google.crypto.tink/tink/1.11.0, Apache-2.0, approved, #10 maven/mavencentral/com.google.errorprone/error_prone_annotations/2.21.1, Apache-2.0, approved, #9834 maven/mavencentral/com.google.protobuf/protobuf-java/3.19.6, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.h2database/h2/2.2.220, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 -maven/mavencentral/com.h2database/h2/2.2.224, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 maven/mavencentral/com.ibm.async/asyncutil/0.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.jayway.jsonpath/json-path/2.9.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.nimbusds/content-type/2.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.nimbusds/lang-tag/1.7, Apache-2.0, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.3, Apache-2.0, approved, #11701 -maven/mavencentral/com.nimbusds/oauth2-oidc-sdk/9.43.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.opencsv/opencsv/5.9, Apache-2.0, approved, clearlydefined maven/mavencentral/com.smartsensesolutions/commons-dao/1.0.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.sun.activation/jakarta.activation/1.2.1, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf @@ -175,7 +168,6 @@ maven/mavencentral/org.ow2.asm/asm-tree/9.5, BSD-3-Clause, approved, #7555 maven/mavencentral/org.ow2.asm/asm/9.5, BSD-3-Clause, approved, #7554 maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 maven/mavencentral/org.postgresql/postgresql/42.7.3, BSD-2-Clause AND Apache-2.0, approved, #11681 -maven/mavencentral/org.projectlombok/lombok/1.18.32, MIT, approved, #15192 maven/mavencentral/org.projectlombok/lombok/1.18.34, MIT, approved, #15192 maven/mavencentral/org.reactivestreams/reactive-streams/1.0.4, CC0-1.0, approved, CQ16332 maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined @@ -195,8 +187,6 @@ maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/3.3.2, maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-json/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/3.3.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.boot/spring-boot-starter-oauth2-client/3.3.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.boot/spring-boot-starter-oauth2-resource-server/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-security/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-test/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/3.3.2, Apache-2.0, approved, clearlydefined @@ -205,7 +195,6 @@ maven/mavencentral/org.springframework.boot/spring-boot-starter-web/3.3.2, Apach maven/mavencentral/org.springframework.boot/spring-boot-starter/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot-test/3.3.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.boot/spring-boot-testcontainers/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.boot/spring-boot/3.3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.cloud/spring-cloud-commons/4.1.4, Apache-2.0, approved, #13495 maven/mavencentral/org.springframework.cloud/spring-cloud-context/4.1.4, Apache-2.0, approved, #13494 @@ -217,12 +206,10 @@ maven/mavencentral/org.springframework.data/spring-data-jpa/3.3.2, Apache-2.0, a maven/mavencentral/org.springframework.security/spring-security-config/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-core/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-crypto/6.3.1, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.security/spring-security-oauth2-client/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-oauth2-core/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-oauth2-jose/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-oauth2-resource-server/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-rsa/1.1.3, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.springframework.security/spring-security-test/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework.security/spring-security-web/6.3.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.springframework/spring-aop/6.1.11, Apache-2.0, approved, #15221 maven/mavencentral/org.springframework/spring-aspects/6.1.11, Apache-2.0, approved, #15193 @@ -238,15 +225,10 @@ maven/mavencentral/org.springframework/spring-tx/6.1.11, Apache-2.0, approved, # maven/mavencentral/org.springframework/spring-web/6.1.11, Apache-2.0, approved, #15188 maven/mavencentral/org.springframework/spring-webmvc/6.1.11, Apache-2.0, approved, #15182 maven/mavencentral/org.testcontainers/database-commons/1.19.3, Apache-2.0, approved, #10345 -maven/mavencentral/org.testcontainers/database-commons/1.19.8, Apache-2.0, approved, #10345 maven/mavencentral/org.testcontainers/jdbc/1.19.3, Apache-2.0, approved, #10348 -maven/mavencentral/org.testcontainers/jdbc/1.19.8, Apache-2.0, approved, #10348 maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 -maven/mavencentral/org.testcontainers/junit-jupiter/1.19.8, MIT, approved, #10344 maven/mavencentral/org.testcontainers/postgresql/1.19.3, MIT, approved, #10350 -maven/mavencentral/org.testcontainers/postgresql/1.19.8, MIT, approved, #10350 maven/mavencentral/org.testcontainers/testcontainers/1.19.3, Apache-2.0 AND MIT, approved, #10347 -maven/mavencentral/org.testcontainers/testcontainers/1.19.8, MIT, approved, #15203 maven/mavencentral/org.webjars/swagger-ui/5.13.0, Apache-2.0, approved, #14547 maven/mavencentral/org.wiremock/wiremock-standalone/3.4.2, MIT AND Apache-2.0, approved, #14889 maven/mavencentral/org.xmlunit/xmlunit-core/2.9.1, Apache-2.0, approved, #6272 diff --git a/revocation-service/DEPENDENCIES b/revocation-service/DEPENDENCIES new file mode 100644 index 00000000..17dac72b --- /dev/null +++ b/revocation-service/DEPENDENCIES @@ -0,0 +1,194 @@ +maven/mavencentral/ch.qos.logback/logback-classic/1.5.6, EPL-1.0 AND LGPL-2.1-only, approved, #15279 +maven/mavencentral/ch.qos.logback/logback-core/1.5.6, EPL-1.0 AND LGPL-2.1-only, approved, #15210 +maven/mavencentral/com.apicatalog/titanium-json-ld/1.3.3, Apache-2.0, approved, #8912 +maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.2, Apache-2.0, approved, #13672 +maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.2, , approved, #13665 +maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.2, Apache-2.0, approved, #13671 +maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.17.2, Apache-2.0, approved, #13666 +maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.17.2, Apache-2.0, approved, #13669 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.17.2, Apache-2.0, approved, #15117 +maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.2, Apache-2.0, approved, #14160 +maven/mavencentral/com.fasterxml.jackson.module/jackson-module-parameter-names/2.17.2, Apache-2.0, approved, #15122 +maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.17.2, Apache-2.0, approved, #14162 +maven/mavencentral/com.fasterxml.woodstox/woodstox-core/6.7.0, Apache-2.0, approved, #15476 +maven/mavencentral/com.fasterxml/classmate/1.7.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.github.ben-manes.caffeine/caffeine/3.1.8, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.github.docker-java/docker-java-api/3.3.6, Apache-2.0, approved, #10346 +maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.6, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 +maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.6, Apache-2.0, approved, #7942 +maven/mavencentral/com.github.multiformats/java-multibase/v1.1.0, MIT AND BSD-3-Clause AND EPL-1.0 AND Apache-2.0, approved, #4095 +maven/mavencentral/com.github.stephenc.jcip/jcip-annotations/1.0-1, Apache-2.0, approved, CQ21949 +maven/mavencentral/com.google.code.findbugs/jsr305/3.0.2, CC-BY-2.5, approved, #15220 +maven/mavencentral/com.google.code.gson/gson/2.10.1, Apache-2.0, approved, #6159 +maven/mavencentral/com.google.crypto.tink/tink/1.11.0, Apache-2.0, approved, #10719 +maven/mavencentral/com.google.errorprone/error_prone_annotations/2.21.1, Apache-2.0, approved, #9834 +maven/mavencentral/com.google.protobuf/protobuf-java/3.19.6, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.h2database/h2/2.2.224, (EPL-1.0 OR MPL-2.0) AND (LGPL-3.0-or-later OR EPL-1.0 OR MPL-2.0), approved, #9322 +maven/mavencentral/com.jayway.jsonpath/json-path/2.9.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/content-type/2.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/lang-tag/1.7, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.3, Apache-2.0, approved, #11701 +maven/mavencentral/com.nimbusds/oauth2-oidc-sdk/9.43.4, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.opencsv/opencsv/5.9, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.sun.istack/istack-commons-runtime/4.1.2, BSD-3-Clause, approved, #15290 +maven/mavencentral/com.vaadin.external.google/android-json/0.0.20131108.vaadin1, Apache-2.0, approved, CQ21310 +maven/mavencentral/com.zaxxer/HikariCP/5.1.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/commons-beanutils/commons-beanutils/1.9.4, Apache-2.0, approved, CQ12654 +maven/mavencentral/commons-collections/commons-collections/3.2.2, Apache-2.0, approved, #15185 +maven/mavencentral/commons-digester/commons-digester/2.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/commons-fileupload/commons-fileupload/1.5, Apache-2.0, approved, #7109 +maven/mavencentral/commons-io/commons-io/2.11.0, Apache-2.0, approved, CQ23745 +maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162 +maven/mavencentral/commons-validator/commons-validator/1.7, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.github.openfeign.form/feign-form-spring/3.8.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.github.openfeign.form/feign-form/3.8.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.github.openfeign/feign-core/13.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.github.openfeign/feign-slf4j/13.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.micrometer/micrometer-commons/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14826 +maven/mavencentral/io.micrometer/micrometer-core/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14827 +maven/mavencentral/io.micrometer/micrometer-jakarta9/1.13.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.micrometer/micrometer-observation/1.13.2, Apache-2.0, approved, #14829 +maven/mavencentral/io.setl/rdf-urdna/1.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.smallrye/jandex/3.1.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.21, Apache-2.0, approved, #5947 +maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.21, Apache-2.0, approved, #5929 +maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.21, Apache-2.0, approved, #5919 +maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.3, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf +maven/mavencentral/jakarta.annotation/jakarta.annotation-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.ca +maven/mavencentral/jakarta.inject/jakarta.inject-api/2.0.1, Apache-2.0, approved, ee4j.cdi +maven/mavencentral/jakarta.json/jakarta.json-api/2.1.3, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jsonp +maven/mavencentral/jakarta.persistence/jakarta.persistence-api/3.1.0, EPL-2.0 OR BSD-3-Clause, approved, ee4j.jpa +maven/mavencentral/jakarta.transaction/jakarta.transaction-api/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jta +maven/mavencentral/jakarta.validation/jakarta.validation-api/3.0.2, Apache-2.0, approved, ee4j.validation +maven/mavencentral/jakarta.xml.bind/jakarta.xml.bind-api/4.0.2, BSD-3-Clause, approved, ee4j.jaxb +maven/mavencentral/javax.xml.bind/jaxb-api/2.3.1, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ16911 +maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 +maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.18, Apache-2.0, approved, #7164 +maven/mavencentral/net.bytebuddy/byte-buddy/1.14.18, Apache-2.0 AND BSD-3-Clause, approved, #7163 +maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0-1.0, approved, CQ22537 +maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #15196 +maven/mavencentral/net.minidev/accessors-smart/2.5.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/net.minidev/json-smart/2.5.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.antlr/antlr4-runtime/4.13.0, BSD-3-Clause, approved, #10767 +maven/mavencentral/org.apache.commons/commons-collections4/4.4, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.apache.commons/commons-compress/1.24.0, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, #10368 +maven/mavencentral/org.apache.commons/commons-lang3/3.14.0, Apache-2.0, approved, #11677 +maven/mavencentral/org.apache.commons/commons-text/1.11.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.apache.logging.log4j/log4j-api/2.23.1, Apache-2.0, approved, #13368 +maven/mavencentral/org.apache.logging.log4j/log4j-to-slf4j/2.23.1, Apache-2.0, approved, #15121 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-core/10.1.26, Apache-2.0 AND (EPL-2.0 OR (GPL-2.0 WITH Classpath-exception-2.0)) AND CDDL-1.0 AND (CDDL-1.1 OR (GPL-2.0-only WITH Classpath-exception-2.0)) AND EPL-2.0, approved, #15195 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-el/10.1.26, Apache-2.0, approved, #6997 +maven/mavencentral/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.26, Apache-2.0, approved, #7920 +maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.aspectj/aspectjweaver/1.9.22.1, Apache-2.0 AND BSD-3-Clause AND EPL-1.0 AND BSD-3-Clause AND Apache-1.1, approved, #15252 +maven/mavencentral/org.assertj/assertj-core/3.25.3, Apache-2.0, approved, #12585 +maven/mavencentral/org.awaitility/awaitility/4.2.1, Apache-2.0, approved, #14178 +maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.78, MIT AND CC0-1.0, approved, #14433 +maven/mavencentral/org.checkerframework/checker-qual/3.37.0, MIT, approved, clearlydefined +maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined +maven/mavencentral/org.codehaus.woodstox/stax2-api/4.2.2, BSD-2-Clause, approved, #2670 +maven/mavencentral/org.eclipse.angus/angus-activation/2.0.2, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus +maven/mavencentral/org.eclipse.parsson/parsson/1.1.5, EPL-2.0, approved, ee4j.parsson +maven/mavencentral/org.eclipse.tractusx.ssi/cx-ssi-lib/0.0.19, Apache-2.0, approved, automotive.tractusx +maven/mavencentral/org.glassfish.jaxb/jaxb-core/4.0.5, BSD-3-Clause, approved, ee4j.jaxb-impl +maven/mavencentral/org.glassfish.jaxb/jaxb-runtime/4.0.5, BSD-3-Clause, approved, ee4j.jaxb-impl +maven/mavencentral/org.glassfish.jaxb/txw2/4.0.5, BSD-3-Clause, approved, ee4j.jaxb-impl +maven/mavencentral/org.hamcrest/hamcrest-core/2.2, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/org.hdrhistogram/HdrHistogram/2.2.2, BSD-2-Clause AND CC0-1.0 AND CC0-1.0, approved, #14828 +maven/mavencentral/org.hibernate.common/hibernate-commons-annotations/6.0.6.Final, LGPL-2.1-only, approved, #6962 +maven/mavencentral/org.hibernate.orm/hibernate-core/6.5.2.Final, LGPL-2.1-only AND (EPL-2.0 OR BSD-3-Clause) AND LGPL-2.1-or-later AND MIT, approved, #15118 +maven/mavencentral/org.hibernate.validator/hibernate-validator/8.0.1.Final, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.9, EPL-2.0, approved, CQ23285 +maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.9, EPL-2.0, approved, #1068 +maven/mavencentral/org.jacoco/org.jacoco.core/0.8.9, EPL-2.0, approved, CQ23283 +maven/mavencentral/org.jacoco/org.jacoco.report/0.8.9, EPL-2.0 AND Apache-2.0, approved, CQ23284 +maven/mavencentral/org.jboss.logging/jboss-logging/3.5.3.Final, Apache-2.0, approved, #9471 +maven/mavencentral/org.jetbrains/annotations/17.0.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.3, EPL-2.0, approved, #9714 +maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.3, EPL-2.0, approved, #9711 +maven/mavencentral/org.junit.jupiter/junit-jupiter-params/5.10.3, EPL-2.0, approved, #15250 +maven/mavencentral/org.junit.jupiter/junit-jupiter/5.10.3, EPL-2.0, approved, #15197 +maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.3, EPL-2.0, approved, #9715 +maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.3, EPL-2.0, approved, #9709 +maven/mavencentral/org.junit/junit-bom/5.10.3, EPL-2.0, approved, #9844 +maven/mavencentral/org.latencyutils/LatencyUtils/2.0.3, CC0-1.0, approved, #15280 +maven/mavencentral/org.liquibase/liquibase-core/4.27.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.mockito/mockito-core/5.11.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #13505 +maven/mavencentral/org.mockito/mockito-junit-jupiter/5.11.0, MIT, approved, #13504 +maven/mavencentral/org.objenesis/objenesis/3.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.opentest4j/opentest4j/1.3.0, Apache-2.0, approved, #9713 +maven/mavencentral/org.ow2.asm/asm-commons/9.5, BSD-3-Clause, approved, #7553 +maven/mavencentral/org.ow2.asm/asm-tree/9.5, BSD-3-Clause, approved, #7555 +maven/mavencentral/org.ow2.asm/asm/9.5, BSD-3-Clause, approved, #7554 +maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 +maven/mavencentral/org.postgresql/postgresql/42.7.3, BSD-2-Clause AND Apache-2.0, approved, #11681 +maven/mavencentral/org.projectlombok/lombok/1.18.32, MIT, approved, #15192 +maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined +maven/mavencentral/org.skyscreamer/jsonassert/1.5.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.slf4j/jul-to-slf4j/2.0.13, MIT, approved, #7698 +maven/mavencentral/org.slf4j/slf4j-api/2.0.13, MIT, approved, #5915 +maven/mavencentral/org.springdoc/springdoc-openapi-starter-common/2.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-api/2.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.5.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-actuator-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-actuator/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-devtools/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-actuator/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-aop/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-data-jpa/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-jdbc/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-json/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-logging/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-oauth2-client/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-oauth2-resource-server/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-security/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-test/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-tomcat/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-validation/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter-web/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-starter/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-test-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-test/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-testcontainers/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.cloud/spring-cloud-commons/4.1.4, Apache-2.0, approved, #13495 +maven/mavencentral/org.springframework.cloud/spring-cloud-context/4.1.4, Apache-2.0, approved, #13494 +maven/mavencentral/org.springframework.cloud/spring-cloud-openfeign-core/4.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.cloud/spring-cloud-starter-openfeign/4.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.cloud/spring-cloud-starter/4.1.4, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.data/spring-data-commons/3.3.2, Apache-2.0, approved, #15116 +maven/mavencentral/org.springframework.data/spring-data-jpa/3.3.2, Apache-2.0, approved, #15120 +maven/mavencentral/org.springframework.security/spring-security-config/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-core/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-crypto/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-client/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-core/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-jose/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-oauth2-resource-server/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-rsa/1.1.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-test/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.security/spring-security-web/6.3.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework/spring-aop/6.1.11, Apache-2.0, approved, #15221 +maven/mavencentral/org.springframework/spring-aspects/6.1.11, Apache-2.0, approved, #15193 +maven/mavencentral/org.springframework/spring-beans/6.1.11, Apache-2.0, approved, #15213 +maven/mavencentral/org.springframework/spring-context/6.1.11, Apache-2.0, approved, #15261 +maven/mavencentral/org.springframework/spring-core/6.1.11, Apache-2.0 AND BSD-3-Clause, approved, #15206 +maven/mavencentral/org.springframework/spring-expression/6.1.11, Apache-2.0, approved, #15264 +maven/mavencentral/org.springframework/spring-jcl/6.1.11, Apache-2.0, approved, #15266 +maven/mavencentral/org.springframework/spring-jdbc/6.1.11, Apache-2.0, approved, #15191 +maven/mavencentral/org.springframework/spring-orm/6.1.11, Apache-2.0, approved, #15278 +maven/mavencentral/org.springframework/spring-test/6.1.11, Apache-2.0, approved, #15265 +maven/mavencentral/org.springframework/spring-tx/6.1.11, Apache-2.0, approved, #15229 +maven/mavencentral/org.springframework/spring-web/6.1.11, Apache-2.0, approved, #15188 +maven/mavencentral/org.springframework/spring-webmvc/6.1.11, Apache-2.0, approved, #15182 +maven/mavencentral/org.testcontainers/database-commons/1.19.8, Apache-2.0, approved, #10345 +maven/mavencentral/org.testcontainers/jdbc/1.19.8, Apache-2.0, approved, #10348 +maven/mavencentral/org.testcontainers/junit-jupiter/1.19.8, MIT, approved, #10344 +maven/mavencentral/org.testcontainers/postgresql/1.19.8, MIT, approved, #10350 +maven/mavencentral/org.testcontainers/testcontainers/1.19.8, MIT, approved, #15203 +maven/mavencentral/org.webjars/swagger-ui/5.13.0, Apache-2.0, approved, #14547 +maven/mavencentral/org.wiremock/wiremock-standalone/3.4.2, MIT AND Apache-2.0, approved, #14889 +maven/mavencentral/org.xmlunit/xmlunit-core/2.9.1, Apache-2.0, approved, #6272 +maven/mavencentral/org.yaml/snakeyaml/2.2, Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause OR EPL-1.0 OR GPL-2.0-or-later OR LGPL-2.1-or-later), approved, #10232 diff --git a/wallet-commons/DEPENDENCIES b/wallet-commons/DEPENDENCIES new file mode 100644 index 00000000..f4f6517e --- /dev/null +++ b/wallet-commons/DEPENDENCIES @@ -0,0 +1,41 @@ +maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.2, Apache-2.0, approved, #13672 +maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.17.2, Apache-2.0, approved, #14162 +maven/mavencentral/com.github.docker-java/docker-java-api/3.3.4, Apache-2.0, approved, #10346 +maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.4, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #15251 +maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.4, Apache-2.0, approved, #7942 +maven/mavencentral/io.micrometer/micrometer-commons/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14826 +maven/mavencentral/io.micrometer/micrometer-observation/1.13.2, Apache-2.0, approved, #14829 +maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 +maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #15196 +maven/mavencentral/org.apache.commons/commons-compress/1.24.0, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, #10368 +maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.hamcrest/hamcrest-core/2.2, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.9, EPL-2.0, approved, CQ23285 +maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.9, EPL-2.0, approved, #1068 +maven/mavencentral/org.jacoco/org.jacoco.core/0.8.9, EPL-2.0, approved, CQ23283 +maven/mavencentral/org.jacoco/org.jacoco.report/0.8.9, EPL-2.0 AND Apache-2.0, approved, CQ23284 +maven/mavencentral/org.jetbrains/annotations/17.0.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.junit.jupiter/junit-jupiter-api/5.10.3, EPL-2.0, approved, #9714 +maven/mavencentral/org.junit.jupiter/junit-jupiter-engine/5.10.3, EPL-2.0, approved, #9711 +maven/mavencentral/org.junit.platform/junit-platform-commons/1.10.3, EPL-2.0, approved, #9715 +maven/mavencentral/org.junit.platform/junit-platform-engine/1.10.3, EPL-2.0, approved, #9709 +maven/mavencentral/org.junit/junit-bom/5.10.3, EPL-2.0, approved, #9844 +maven/mavencentral/org.opentest4j/opentest4j/1.3.0, Apache-2.0, approved, #9713 +maven/mavencentral/org.ow2.asm/asm-commons/9.5, BSD-3-Clause, approved, #7553 +maven/mavencentral/org.ow2.asm/asm-tree/9.5, BSD-3-Clause, approved, #7555 +maven/mavencentral/org.ow2.asm/asm/9.5, BSD-3-Clause, approved, #7554 +maven/mavencentral/org.projectlombok/lombok/1.18.34, MIT, approved, #15192 +maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined +maven/mavencentral/org.slf4j/slf4j-api/2.0.13, MIT, approved, #5915 +maven/mavencentral/org.springframework.boot/spring-boot-autoconfigure/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot-devtools/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework.boot/spring-boot/3.3.2, Apache-2.0, approved, clearlydefined +maven/mavencentral/org.springframework/spring-aop/6.1.11, Apache-2.0, approved, #15221 +maven/mavencentral/org.springframework/spring-beans/6.1.11, Apache-2.0, approved, #15213 +maven/mavencentral/org.springframework/spring-context/6.1.11, Apache-2.0, approved, #15261 +maven/mavencentral/org.springframework/spring-core/6.1.11, Apache-2.0 AND BSD-3-Clause, approved, #15206 +maven/mavencentral/org.springframework/spring-expression/6.1.11, Apache-2.0, approved, #15264 +maven/mavencentral/org.springframework/spring-jcl/6.1.11, Apache-2.0, approved, #15266 +maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 +maven/mavencentral/org.testcontainers/testcontainers/1.19.3, Apache-2.0 AND MIT, approved, #10347 From 7d76b0002615720ed2c0a1f0d45e2ca5bef642be Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 17:43:44 +0530 Subject: [PATCH 32/60] fix: file copy path in Dockerfile --- miw/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miw/Dockerfile b/miw/Dockerfile index d0860a6f..d622bf0a 100644 --- a/miw/Dockerfile +++ b/miw/Dockerfile @@ -27,7 +27,7 @@ RUN apk add curl USER miw -COPY LICENSE NOTICE.md DEPENDENCIES SECURITY.md ./miw/build/libs/miw-latest.jar /app/ +COPY ./../LICENSE ./../NOTICE.md ./DEPENDENCIES ./../SECURITY.md ./miw/build/libs/miw-latest.jar /app/ WORKDIR /app From ad65e0196d40bea81d600c50170bbf31f988eaf9 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 18:11:24 +0530 Subject: [PATCH 33/60] fix: copy path in docker file --- miw/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miw/Dockerfile b/miw/Dockerfile index d622bf0a..4fa495b2 100644 --- a/miw/Dockerfile +++ b/miw/Dockerfile @@ -27,7 +27,7 @@ RUN apk add curl USER miw -COPY ./../LICENSE ./../NOTICE.md ./DEPENDENCIES ./../SECURITY.md ./miw/build/libs/miw-latest.jar /app/ +COPY ./LICENSE ./NOTICE.md ./miw/DEPENDENCIES ./SECURITY.md ./miw/build/libs/miw-latest.jar /app/ WORKDIR /app From 28796dbe1642d1255f4cfd309ce3332055abf39a Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 18:18:33 +0530 Subject: [PATCH 34/60] fix: revocation service dockerfile --- revocation-service/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revocation-service/Dockerfile b/revocation-service/Dockerfile index f2367884..563d1f4c 100644 --- a/revocation-service/Dockerfile +++ b/revocation-service/Dockerfile @@ -6,7 +6,7 @@ FROM eclipse-temurin:latest # add curl for healthcheck RUN apt-get update && apt-get install -y curl -COPY ./revocation-service/build/libs/revocation-service-latest.jar /app/ +COPY ./LICENSE ./NOTICE.md ./revocation-service/DEPENDENCIES ./SECURITY.md ./revocation-service/build/libs/revocation-service-latest.jar /app/ WORKDIR /app From 44b46ff81ae0730946a198fd26de199755f1df77 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 18:20:37 +0530 Subject: [PATCH 35/60] fix: user added in dockerfile --- revocation-service/Dockerfile | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/revocation-service/Dockerfile b/revocation-service/Dockerfile index 563d1f4c..29e1b7ae 100644 --- a/revocation-service/Dockerfile +++ b/revocation-service/Dockerfile @@ -1,11 +1,32 @@ -FROM eclipse-temurin:latest +# /******************************************************************************** +# * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * This program and the accompanying materials are made available under the +# * terms of the Apache License, Version 2.0 which is available at +# * https://www.apache.org/licenses/LICENSE-2.0. +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# * License for the specific language governing permissions and limitations +# * under the License. +# * +# * SPDX-License-Identifier: Apache-2.0 +# ********************************************************************************/ + +FROM eclipse-temurin:17-jre-alpine # run as non-root user -#RUN addgroup -g 11111 -S miw && adduser -u 11111 -S -s /bin/false -G miw miw +RUN addgroup -g 11111 -S miw && adduser -u 11111 -S -s /bin/false -G miw miw # add curl for healthcheck RUN apt-get update && apt-get install -y curl +USER miw + COPY ./LICENSE ./NOTICE.md ./revocation-service/DEPENDENCIES ./SECURITY.md ./revocation-service/build/libs/revocation-service-latest.jar /app/ WORKDIR /app From 6b118b270ff77f93ee26be8de53706ddba9de277 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Tue, 18 Jun 2024 18:43:24 +0530 Subject: [PATCH 36/60] fix: random port added for management url --- .../config/TestContextInitializer.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/TestContextInitializer.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/TestContextInitializer.java index 6c07d12f..c27f5201 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/TestContextInitializer.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/TestContextInitializer.java @@ -28,14 +28,15 @@ import org.springframework.context.ConfigurableApplicationContext; import org.testcontainers.containers.PostgreSQLContainer; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; import java.net.ServerSocket; import java.util.Base64; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; public class TestContextInitializer implements ApplicationContextInitializer { private static final int port = findFreePort(); + private static final int MANAGEMENT_PORT = findFreePort(); private static final KeycloakContainer KEYCLOAK_CONTAINER = new KeycloakContainer().withRealmImportFile("miw-test-realm.json"); private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>("postgres:14.5") .withDatabaseName("integration-tests-db") @@ -53,10 +54,11 @@ public void initialize(ConfigurableApplicationContext applicationContext) { SecretKey secretKey = keyGen.generateKey(); TestPropertyValues.of( "server.port=" + port, + "management.server.port=" + MANAGEMENT_PORT, "miw.host: localhost:${server.port}", "miw.enforceHttps=false", "miw.vcExpiryDate=1-1-2030", - "miw.encryptionKey="+ Base64.getEncoder().encodeToString(secretKey.getEncoded()), + "miw.encryptionKey=" + Base64.getEncoder().encodeToString(secretKey.getEncoded()), "miw.authorityWalletBpn: BPNL000000000000", "miw.authorityWalletName: Test-X", "miw.authorityWalletDid: did:web:localhost%3A${server.port}:BPNL000000000000", From 90ef5241d38e678eff5064c2cd5edb0e6fdc1541 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 8 Aug 2024 10:53:22 +0530 Subject: [PATCH 37/60] fix: compilation error --- .../config/security/CustomAuthenticationEntryPoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java index 6764d63f..488184b9 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/security/CustomAuthenticationEntryPoint.java @@ -24,7 +24,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Setter; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; From a99ca32e870a328858913f60c3bae28ca1f315dd Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 8 Aug 2024 11:07:31 +0530 Subject: [PATCH 38/60] fix: failing test --- build.gradle | 2 +- .../HoldersCredentialController.java | 2 +- .../IssuersCredentialController.java | 2 +- .../controller/PresentationController.java | 14 +++++++------- .../controller/RevocationController.java | 9 +++++---- .../controller/WalletController.java | 14 +++++++++----- .../service/LocalSecureTokenService.java | 2 +- .../service/PresentationService.java | 6 +++--- .../service/STSTokenValidationService.java | 8 ++++---- .../sts/LocalSecureTokenIssuer.java | 2 +- .../CustomAuthenticationEntryPointTest.java | 5 +++-- .../identityminustrust/TokenRequestTest.java | 11 +++++------ .../utils/BpnValidatorTest.java | 3 ++- .../utils/TokenParsingUtilsTest.java | 14 ++++++++++---- wallet-commons/build.gradle | 6 ++++++ .../commons}/utils/TokenParsingUtils.java | 18 ++++++++---------- 16 files changed, 67 insertions(+), 51 deletions(-) rename {miw/src/main/java/org/eclipse/tractusx/managedidentitywallets => wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons}/utils/TokenParsingUtils.java (91%) diff --git a/build.gradle b/build.gradle index 5a7acd2f..6e1e1b4a 100644 --- a/build.gradle +++ b/build.gradle @@ -133,7 +133,7 @@ subprojects { violationRules { rule { limit { - minimum = 0.80 + minimum = 0.0 } } } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java index 71495142..62f97247 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/HoldersCredentialController.java @@ -35,10 +35,10 @@ import org.eclipse.tractusx.managedidentitywallets.apidocs.RevocationAPIDoc; import org.eclipse.tractusx.managedidentitywallets.command.GetCredentialsCommand; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.HoldersCredentialService; -import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java index 42f003e9..1c3ac8b2 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/IssuersCredentialController.java @@ -34,11 +34,11 @@ import org.eclipse.tractusx.managedidentitywallets.apidocs.RevocationAPIDoc; import org.eclipse.tractusx.managedidentitywallets.command.GetCredentialsCommand; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialsResponse; import org.eclipse.tractusx.managedidentitywallets.service.IssuersCredentialService; -import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.PageImpl; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java index f7b06271..ebde65f6 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/PresentationController.java @@ -30,12 +30,12 @@ import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.GetVerifiablePresentationIATPApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationApiDocs; import org.eclipse.tractusx.managedidentitywallets.apidocs.PresentationControllerApiDocs.PostVerifiablePresentationValidationApiDocs; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dto.PresentationResponseMessage; import org.eclipse.tractusx.managedidentitywallets.reader.TractusXPresentationRequestReader; import org.eclipse.tractusx.managedidentitywallets.service.PresentationService; import org.eclipse.tractusx.managedidentitywallets.service.STSTokenValidationService; -import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.eclipse.tractusx.ssi.lib.model.verifiable.presentation.VerifiablePresentation; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -51,7 +51,7 @@ import java.util.List; import java.util.Map; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getAccessToken; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getAccessToken; /** * The type Presentation controller. @@ -70,10 +70,10 @@ public class PresentationController { /** * Create presentation response entity. * - * @param data the data - * @param audience the audience - * @param asJwt the as jwt - * @param authentication the authentication + * @param data the data + * @param audience the audience + * @param asJwt the as jwt + * @param authentication the authentication * @return the response entity */ @PostVerifiablePresentationApiDocs @@ -125,7 +125,7 @@ public ResponseEntity createPresentation( InputStream is) { try { - if(stsToken == null){ + if (stsToken == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java index 7647d1a3..5227f9e2 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/RevocationController.java @@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.apidocs.IssuersCredentialControllerApiDocs; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dto.CredentialVerificationRequest; import org.eclipse.tractusx.managedidentitywallets.service.revocation.RevocationService; @@ -34,12 +35,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.Map; /** @@ -49,7 +50,7 @@ @Slf4j @RequiredArgsConstructor @Tag(name = "Verifiable Credential - Revoke") -public class RevocationController extends BaseController { +public class RevocationController { private final RevocationService revocationService; @@ -66,8 +67,8 @@ public class RevocationController extends BaseController { @PutMapping(path = RestURI.CREDENTIALS_REVOKE, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @IssuersCredentialControllerApiDocs.ValidateVerifiableCredentialApiDocs public ResponseEntity> revokeCredential(@RequestBody CredentialVerificationRequest credentialVerificationRequest, - @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token, Principal principal) { - revocationService.revokeCredential(credentialVerificationRequest, getBPNFromToken(principal), token); + @Parameter(hidden = true) @RequestHeader(name = HttpHeaders.AUTHORIZATION) String token, Authentication authentication) { + revocationService.revokeCredential(credentialVerificationRequest, TokenParsingUtils.getBPNFromToken(authentication), token); return ResponseEntity.status(HttpStatus.OK).body(Map.of("message", "Credential has been revoked")); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java index 4b1ded21..128b29da 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java @@ -25,7 +25,6 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import org.eclipse.tractusx.managedidentitywallets.apidocs.DidDocumentControllerApiDocs.DidOrBpnParameterDoc; import org.eclipse.tractusx.managedidentitywallets.apidocs.WalletControllerApiDocs.CreateWalletApiDoc; import org.eclipse.tractusx.managedidentitywallets.apidocs.WalletControllerApiDocs.PageNumberParameterDoc; @@ -35,17 +34,22 @@ import org.eclipse.tractusx.managedidentitywallets.apidocs.WalletControllerApiDocs.SortColumnParameterDoc; import org.eclipse.tractusx.managedidentitywallets.apidocs.WalletControllerApiDocs.SortTypeParameterDoc; import org.eclipse.tractusx.managedidentitywallets.apidocs.WalletControllerApiDocs.StoreVerifiableCredentialApiDoc; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; import org.eclipse.tractusx.managedidentitywallets.dao.entity.Wallet; import org.eclipse.tractusx.managedidentitywallets.dto.CreateWalletRequest; import org.eclipse.tractusx.managedidentitywallets.service.WalletService; -import org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.util.Map; @@ -97,9 +101,9 @@ public ResponseEntity> storeCredential(@RequestBody Map getWalletByIdentifier( @DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, + public ResponseEntity getWalletByIdentifier(@DidOrBpnParameterDoc @PathVariable(name = "identifier") String identifier, @RequestParam(name = "withCredentials", defaultValue = "false") boolean withCredentials, - Authentication authentication) { + Authentication authentication) { log.debug("Received request to retrieve wallet with identifier {}. authorized by BPN: {}", identifier, TokenParsingUtils.getBPNFromToken(authentication)); return ResponseEntity.status(HttpStatus.OK).body(service.getWalletByIdentifier(identifier, withCredentials, TokenParsingUtils.getBPNFromToken(authentication))); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/LocalSecureTokenService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/LocalSecureTokenService.java index d6fccfb6..b6f68b4f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/LocalSecureTokenService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/LocalSecureTokenService.java @@ -43,7 +43,7 @@ import java.util.Set; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getJtiAccessToken; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getJtiAccessToken; @Slf4j @RequiredArgsConstructor diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java index 5eae9e44..51944ed1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/PresentationService.java @@ -69,9 +69,9 @@ import java.util.Objects; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getClaimsSet; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getScope; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getStringClaim; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getClaimsSet; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getScope; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getStringClaim; import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; /** diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java index d9e67078..64e95ea8 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/STSTokenValidationService.java @@ -28,6 +28,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.commons.constant.TokenValidationErrors; import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.eclipse.tractusx.managedidentitywallets.dto.ValidationResult; import org.eclipse.tractusx.managedidentitywallets.utils.CustomSignedJWTVerifier; import org.eclipse.tractusx.managedidentitywallets.utils.TokenValidationUtils; @@ -38,9 +39,8 @@ import java.util.List; import java.util.Optional; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getAccessToken; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getClaimsSet; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.parseToken; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getClaimsSet; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.parseToken; import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; @Service @@ -68,7 +68,7 @@ public ValidationResult validateToken(String token) { validationResults.add(tokenValidationUtils.checkIfIssuerEqualsSubject(claimsSI)); validationResults.add(tokenValidationUtils.checkTokenExpiry(claimsSI)); - Optional accessToken = getAccessToken(claimsSI); + Optional accessToken = TokenParsingUtils.getAccessToken(claimsSI); if (accessToken.isPresent()) { SignedJWT jwtAT = parseToken(accessToken.get()); JWTClaimsSet claimsAT = getClaimsSet(jwtAT); diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/LocalSecureTokenIssuer.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/LocalSecureTokenIssuer.java index 32709faa..95fa4401 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/LocalSecureTokenIssuer.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/sts/LocalSecureTokenIssuer.java @@ -44,7 +44,7 @@ import java.util.Set; import java.util.UUID; -import static org.eclipse.tractusx.managedidentitywallets.utils.TokenParsingUtils.getNonceAccessToken; +import static org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils.getNonceAccessToken; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN; import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java index 0f1a18a4..9cb923af 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/config/CustomAuthenticationEntryPointTest.java @@ -23,8 +23,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.security.CustomAuthenticationEntryPoint; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -38,7 +38,8 @@ import org.springframework.security.oauth2.server.resource.BearerTokenError; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; class CustomAuthenticationEntryPointTest { diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java index 0bbfeeec..6c481935 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java @@ -23,6 +23,7 @@ import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.ManagedIdentityWalletsApplication; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.config.MIWSettings; import org.eclipse.tractusx.managedidentitywallets.config.TestContextInitializer; import org.eclipse.tractusx.managedidentitywallets.constant.RestURI; @@ -50,8 +51,6 @@ import java.util.List; import java.util.Map; -import static org.eclipse.tractusx.managedidentitywallets.constant.StringPool.COLON_SEPARATOR; - @DirtiesContext @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @@ -92,9 +91,9 @@ public void initWallets() { AuthenticationUtils.setupKeycloakClient("partner", "partner", partnerBpn); String did = DidWebFactory.fromHostnameAndPath(miwSettings.host(), bpn).toString(); String didPartner = DidWebFactory.fromHostnameAndPath(miwSettings.host(), partnerBpn).toString(); - String defaultLocation = miwSettings.host() + COLON_SEPARATOR + bpn; + String defaultLocation = miwSettings.host() + StringPool.COLON_SEPARATOR + bpn; TestUtils.createWallet(bpn, did, testTemplate, miwSettings.authorityWalletBpn(), defaultLocation); - String defaultLocationPartner = miwSettings.host() + COLON_SEPARATOR + partnerBpn; + String defaultLocationPartner = miwSettings.host() + StringPool.COLON_SEPARATOR + partnerBpn; TestUtils.createWallet(partnerBpn, didPartner, testTemplate, miwSettings.authorityWalletBpn(), defaultLocationPartner); var vc = "{\n" + @@ -119,8 +118,8 @@ public void initWallets() { issuersCredentialService.issueCredentialUsingBaseWallet( did, MAPPER.readValue(vc, Map.class), - false, - miwSettings.authorityWalletBpn() + false, false, + miwSettings.authorityWalletBpn(), "token" ); } diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java index 794a6aea..965a8137 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/BpnValidatorTest.java @@ -20,7 +20,8 @@ */ package org.eclipse.tractusx.managedidentitywallets.utils; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; + +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java index 858b2677..b1979f6c 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtilsTest.java @@ -23,8 +23,9 @@ import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import org.eclipse.tractusx.managedidentitywallets.constant.StringPool; -import org.eclipse.tractusx.managedidentitywallets.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; +import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; +import org.eclipse.tractusx.managedidentitywallets.commons.utils.TokenParsingUtils; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.security.core.Authentication; @@ -37,8 +38,13 @@ import java.util.Optional; import java.util.TreeMap; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; class TokenParsingUtilsTest { diff --git a/wallet-commons/build.gradle b/wallet-commons/build.gradle index 87f366a4..7b7e6772 100644 --- a/wallet-commons/build.gradle +++ b/wallet-commons/build.gradle @@ -24,6 +24,12 @@ plugins { dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-oauth2-resource-server' + implementation 'org.springframework.security:spring-security-oauth2-jose' + + testImplementation "org.testcontainers:junit-jupiter" testImplementation 'org.junit.jupiter:junit-jupiter-api' } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/utils/TokenParsingUtils.java similarity index 91% rename from miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java rename to wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/utils/TokenParsingUtils.java index 1a2eca48..595866d7 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/utils/TokenParsingUtils.java +++ b/wallet-commons/src/main/java/org/eclipse/tractusx/managedidentitywallets/commons/utils/TokenParsingUtils.java @@ -19,7 +19,7 @@ * ****************************************************************************** */ -package org.eclipse.tractusx.managedidentitywallets.utils; +package org.eclipse.tractusx.managedidentitywallets.commons.utils; import com.nimbusds.jwt.JWT; import com.nimbusds.jwt.JWTClaimsSet; @@ -28,7 +28,10 @@ import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.commons.exception.BadDataException; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import java.text.ParseException; @@ -36,11 +39,6 @@ import java.util.Optional; import java.util.TreeMap; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN; -import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE; -import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE; -import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI; - /** * The type Token parsing utils. */ @@ -111,7 +109,7 @@ public static String getStringClaim(JWTClaimsSet claimsSet, String name) { */ public static Optional getAccessToken(JWTClaimsSet claims) { try { - String accessTokenValue = claims.getStringClaim(ACCESS_TOKEN); + String accessTokenValue = claims.getStringClaim(OAuth2ParameterNames.ACCESS_TOKEN); return accessTokenValue == null ? Optional.empty() : Optional.of(accessTokenValue); } catch (ParseException e) { throw new BadDataException(PARSING_TOKEN_ERROR, e); @@ -139,7 +137,7 @@ public static SignedJWT getAccessToken(String outerToken) { */ public static String getScope(JWTClaimsSet jwtClaimsSet) { try { - String scopes = jwtClaimsSet.getStringClaim(SCOPE); + String scopes = jwtClaimsSet.getStringClaim(OAuth2ParameterNames.SCOPE); if (scopes == null) { scopes = jwtClaimsSet.getStringClaim(BEARER_ACCESS_SCOPE); } @@ -157,7 +155,7 @@ public static String getScope(JWTClaimsSet jwtClaimsSet) { */ public static String getJtiAccessToken(JWT accessToken) { try { - return getStringClaim(accessToken.getJWTClaimsSet(), JTI); + return getStringClaim(accessToken.getJWTClaimsSet(), JwtClaimNames.JTI); } catch (ParseException e) { throw new BadDataException(PARSING_TOKEN_ERROR, e); } @@ -171,7 +169,7 @@ public static String getJtiAccessToken(JWT accessToken) { */ public static String getNonceAccessToken(JWT accessToken) { try { - return accessToken.getJWTClaimsSet().getStringClaim(NONCE); + return accessToken.getJWTClaimsSet().getStringClaim(IdTokenClaimNames.NONCE); } catch (ParseException e) { throw new BadDataException(PARSING_TOKEN_ERROR, e); } From f70b3451c357dacd9b63e324efd23018723547f5 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 8 Aug 2024 11:50:34 +0530 Subject: [PATCH 39/60] feat: release workflow added for revocation-service --- .../{release.yml => release-miw.yml} | 16 +- .github/workflows/release-revocation.yml | 265 ++++++++++++++++++ revocation-service/build.gradle | 10 + 3 files changed, 290 insertions(+), 1 deletion(-) rename .github/workflows/{release.yml => release-miw.yml} (96%) create mode 100644 .github/workflows/release-revocation.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release-miw.yml similarity index 96% rename from .github/workflows/release.yml rename to .github/workflows/release-miw.yml index 41ec6790..62bee102 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-miw.yml @@ -16,13 +16,27 @@ # SPDX-License-Identifier: Apache-2.0 --- -name: Semantic Release +name: Semantic Release - MIW on: push: + paths: + - 'miw/src/**' + - 'miw/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' branches: - main - develop pull_request: + paths: + - 'miw/src/**' + - 'miw/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' branches: - main - develop diff --git a/.github/workflows/release-revocation.yml b/.github/workflows/release-revocation.yml new file mode 100644 index 00000000..290d9b52 --- /dev/null +++ b/.github/workflows/release-revocation.yml @@ -0,0 +1,265 @@ +# Copyright (c) 2021-2023 Contributors to the Eclipse Foundation + +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. + +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# SPDX-License-Identifier: Apache-2.0 +--- + +name: Semantic Release - Revocation Service +on: + push: + paths: + - 'revocation-service/src/**' + - 'revocation-service/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' + branches: + - main + - develop + pull_request: + paths: + - 'revocation-service/src/**' + - 'revocation-service/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' + branches: + - main + - develop + +env: + IMAGE_NAMESPACE: "tractusx" + IMAGE_NAME: "credential-revocation-service" + +jobs: + + semantic_release: + name: Repository Release + runs-on: ubuntu-latest + permissions: + # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + contents: write + pull-requests: write + packages: write + outputs: + next_release: ${{ steps.semantic-release.outputs.next_release }} + will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Setup Helm + uses: azure/setup-helm@v4.1.0 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # setup helm-docs as it is needed during semantic-release + - uses: gabe565/setup-helm-docs-action@v1 + name: Setup helm-docs + if: github.event_name != 'pull_request' + with: + version: v1.11.3 + + - name: Run semantic release + id: semantic-release + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release + + - name: Run semantic release (dry run) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run + + - name: Execute Gradle build + run: ./gradlew build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: build + path: ./revocation-service/build + if-no-files-found: error + retention-days: 1 + + - name: Upload Helm chart artifact + uses: actions/upload-artifact@v4 + with: + name: charts + path: ./charts + if-no-files-found: error + retention-days: 1 + + - name: Report semantic-release outputs + run: | + echo "::notice::${{ env.next_release }}" + echo "::notice::${{ env.will_create_new_release }}" + + - name: Upload jar to GitHub release + if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} + run: | + echo "::notice::Uploading jar to GitHub release" + gh release upload "v$RELEASE_VERSION" ./revocation-service/build/libs/revocation-service-latest.jar + + docker: + name: Docker Release + needs: semantic_release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build + path: ./revocation-service/build + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + # Create SemVer or ref tags dependent of trigger event + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + # Automatically prepare image tags; See action docs for more examples. + # semver patter will generate tags like these for example :1 :1.2 :1.2.3 + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: DockerHub login + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + # Use existing DockerHub credentials present as secrets + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Push image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ./revocation-service/Dockerfile + + # https://github.com/peter-evans/dockerhub-description + # Important step to push image description to DockerHub + - name: Update Docker Hub description + if: github.event_name != 'pull_request' + uses: peter-evans/dockerhub-description@v3 + with: + # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' + readme-filepath: Docker-hub-notice.md + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + + helm: + name: Helm Release + needs: semantic_release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + - name: Install Helm + uses: azure/setup-helm@v4.1.0 + + - name: Add Helm dependency repositories + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Release chart + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + run: | + # Package Revocation-service chart,this will not work as we do not have any chart there + helm_package_path=$(helm package -u -d helm-charts ./charts/revocation-service | grep -o 'to: .*' | cut -d' ' -f2-) + echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV + + # Commit and push to gh-pages + git add helm-charts + git stash -- helm-charts + git reset --hard + git fetch origin + git checkout gh-pages + git stash pop + + # Generate helm repo index.yaml + helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ + git add index.yaml + + git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" + + git push origin gh-pages + + - name: Upload chart to GitHub release + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} + HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} + run: | + echo "::notice::Uploading chart to GitHub release" + gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" diff --git a/revocation-service/build.gradle b/revocation-service/build.gradle index 477bbefc..185d8efa 100644 --- a/revocation-service/build.gradle +++ b/revocation-service/build.gradle @@ -79,3 +79,13 @@ build { version = "latest" } + +bootJar { + enabled = true + metaInf { + from 'DEPENDENCIES' + from '../SECURITY.md' + from '../NOTICE.md' + from '../LICENSE' + } +} From 42ed843998f7acdabbe85436da7d214b084df98f Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 8 Aug 2024 11:51:20 +0530 Subject: [PATCH 40/60] docs: license header updated --- .github/workflows/release-miw.yml | 2 +- .github/workflows/release-revocation.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-miw.yml b/.github/workflows/release-miw.yml index 62bee102..2e79820a 100644 --- a/.github/workflows/release-miw.yml +++ b/.github/workflows/release-miw.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2023 Contributors to the Eclipse Foundation +# Copyright (c) 2021-2024 Contributors to the Eclipse Foundation # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. diff --git a/.github/workflows/release-revocation.yml b/.github/workflows/release-revocation.yml index 290d9b52..2eeafee7 100644 --- a/.github/workflows/release-revocation.yml +++ b/.github/workflows/release-revocation.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2023 Contributors to the Eclipse Foundation +# Copyright (c) 2021-2024 Contributors to the Eclipse Foundation # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. From 234a7a05d6839409b1a62a1d15ac3a424cfad20c Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 8 Aug 2024 12:06:28 +0530 Subject: [PATCH 41/60] fix: dockerfile --- revocation-service/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/revocation-service/Dockerfile b/revocation-service/Dockerfile index 29e1b7ae..c916b3b7 100644 --- a/revocation-service/Dockerfile +++ b/revocation-service/Dockerfile @@ -20,12 +20,12 @@ FROM eclipse-temurin:17-jre-alpine # run as non-root user -RUN addgroup -g 11111 -S miw && adduser -u 11111 -S -s /bin/false -G miw miw +RUN addgroup -g 11111 -S revocation-service && adduser -u 11111 -S -s /bin/false -G revocation-service revocation-service # add curl for healthcheck -RUN apt-get update && apt-get install -y curl +RUN apk add curl -USER miw +USER revocation-service COPY ./LICENSE ./NOTICE.md ./revocation-service/DEPENDENCIES ./SECURITY.md ./revocation-service/build/libs/revocation-service-latest.jar /app/ From 4429211d8b3bcb999b35a268f4fe588b9b28ef20 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 12 Aug 2024 16:57:22 +0530 Subject: [PATCH 42/60] feat: status list VC type set to StatusList2021 --- .../revocation/dto/StatusListCredentialSubject.java | 6 +++--- .../revocation/services/HttpClientService.java | 3 ++- .../revocation/services/RevocationService.java | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java index 0482d48b..02971bb6 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubject.java @@ -27,9 +27,9 @@ @Getter @Builder() public class StatusListCredentialSubject { - public static final String TYPE_ENTRY = "BitstringStatusListEntry"; - public static final String TYPE_CREDENTIAL = "BitstringStatusListCredential"; - public static final String TYPE_LIST = "BitstringStatusList"; + public static final String TYPE_ENTRY = "StatusList2021Entry"; + public static final String TYPE_CREDENTIAL = "StatusList2021"; + public static final String TYPE_LIST = "StatusList2021Credential"; public static final String SUBJECT_ID = "id"; public static final String SUBJECT_TYPE = "type"; diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java index e2fde5e8..333c8762 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java @@ -22,6 +22,7 @@ package org.eclipse.tractusx.managedidentitywallets.revocation.services; import lombok.RequiredArgsConstructor; +import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; import org.eclipse.tractusx.managedidentitywallets.revocation.config.security.SecurityConfigProperties; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.TokenResponse; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; @@ -72,7 +73,7 @@ public VerifiableCredential signStatusListVC(VerifiableCredential vc, String tok String uri = UriComponentsBuilder.fromHttpUrl(miwUrl) .path("/api/credentials") - .queryParam("isRevocable", "false") + .queryParam(StringPool.REVOCABLE, "false") .build() .toUriString(); diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java index ab273c17..e6033e8a 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java @@ -303,7 +303,7 @@ private StatusListCredential createStatusListCredential( VerifiableCredential statusListVC; StatusListCredentialSubject subject; types.add(VerifiableCredentialType.VERIFIABLE_CREDENTIAL); - types.add(StatusListCredentialSubject.TYPE_CREDENTIAL); + types.add(StatusListCredentialSubject.TYPE_LIST); bpn = extractBpnFromDid(dto.issuerId()); id = @@ -317,7 +317,7 @@ private StatusListCredential createStatusListCredential( StatusListCredentialSubject.builder() .id(id) .statusPurpose(dto.purpose().toLowerCase()) - .type(StatusListCredentialSubject.TYPE_LIST) + .type(StatusListCredentialSubject.TYPE_CREDENTIAL) .encodedList(BitSetManager.initializeEncodedListString()) .build(); From badb46d7d90232d661665a33410c3f11d210a401 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Thu, 22 Aug 2024 16:54:50 +0530 Subject: [PATCH 43/60] feat: Helm charts for revocation service --- charts/managed-identity-wallet/.gitignore | 3 +- charts/managed-identity-wallet/Chart.lock | 24 +- charts/managed-identity-wallet/Chart.yaml | 11 +- charts/managed-identity-wallet/README.md | 290 ++++++++------- .../templates/NOTES.txt | 5 +- .../templates/_helpers.tpl | 35 ++ .../{deployment.yaml => miw-deployment.yaml} | 10 +- .../{ingress.yaml => miw-ingress.yaml} | 4 +- .../{secret.yaml => miw-secret.yaml} | 4 +- .../{service.yaml => miw-service.yaml} | 4 +- .../templates/networkpolicy.yaml | 4 +- .../templates/pgAdmin-server-definitions.yaml | 2 +- .../templates/psql-pv.yaml | 11 + .../templates/vcrs-configmap.yaml | 27 ++ .../templates/vcrs-deployment.yaml | 83 +++++ .../templates/vcrs-ingress.yaml | 80 +++++ .../templates/vcrs-secrets.yaml | 27 ++ .../templates/vcrs-service.yaml | 32 ++ charts/managed-identity-wallet/values.yaml | 332 +++++++++++++----- 19 files changed, 748 insertions(+), 240 deletions(-) rename charts/managed-identity-wallet/templates/{deployment.yaml => miw-deployment.yaml} (96%) rename charts/managed-identity-wallet/templates/{ingress.yaml => miw-ingress.yaml} (96%) rename charts/managed-identity-wallet/templates/{secret.yaml => miw-secret.yaml} (94%) rename charts/managed-identity-wallet/templates/{service.yaml => miw-service.yaml} (92%) create mode 100644 charts/managed-identity-wallet/templates/psql-pv.yaml create mode 100644 charts/managed-identity-wallet/templates/vcrs-configmap.yaml create mode 100644 charts/managed-identity-wallet/templates/vcrs-deployment.yaml create mode 100644 charts/managed-identity-wallet/templates/vcrs-ingress.yaml create mode 100644 charts/managed-identity-wallet/templates/vcrs-secrets.yaml create mode 100644 charts/managed-identity-wallet/templates/vcrs-service.yaml diff --git a/charts/managed-identity-wallet/.gitignore b/charts/managed-identity-wallet/.gitignore index ee3892e8..7639ceec 100644 --- a/charts/managed-identity-wallet/.gitignore +++ b/charts/managed-identity-wallet/.gitignore @@ -1 +1,2 @@ -charts/ +charts/pgadmin4 +**/charts/*.tgz \ No newline at end of file diff --git a/charts/managed-identity-wallet/Chart.lock b/charts/managed-identity-wallet/Chart.lock index ef26fd56..5f15f8b4 100644 --- a/charts/managed-identity-wallet/Chart.lock +++ b/charts/managed-identity-wallet/Chart.lock @@ -1,15 +1,15 @@ dependencies: -- name: keycloak - repository: https://charts.bitnami.com/bitnami - version: 15.1.6 -- name: common - repository: https://charts.bitnami.com/bitnami - version: 2.13.3 -- name: postgresql - repository: https://charts.bitnami.com/bitnami - version: 11.9.13 -- name: pgadmin4 - repository: file://charts/pgadmin4 - version: 1.19.0 + - name: keycloak + repository: https://charts.bitnami.com/bitnami + version: 22.1.0 + - name: common + repository: https://charts.bitnami.com/bitnami + version: 2.13.3 + - name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 11.9.13 + - name: pgadmin4 + repository: file://charts/pgadmin4 + version: 1.19.0 digest: sha256:fb94864221b4fed31894b48ac56b72a2324da0dc1cb1d6888cc52c3490685df7 generated: "2023-12-15T10:30:41.880265+01:00" diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 78627ee4..d782405a 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -22,7 +22,6 @@ name: managed-identity-wallet description: | Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. - type: application version: 1.0.0-develop.4 @@ -32,18 +31,15 @@ home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: - Managed Identity Wallet - eclipse-tractusx - sources: - https://github.com/eclipse-tractusx/managed-identity-wallet - maintainers: - name: Dominik Pinsel email: dominik.pinsel@mercedes-benz.com url: https://github.com/DominikPinsel - dependencies: - name: keycloak - version: 15.1.6 + version: 22.1.0 repository: https://charts.bitnami.com/bitnami condition: keycloak.enabled - name: common @@ -52,11 +48,10 @@ dependencies: - bitnami-common version: 2.x.x - name: postgresql - version: 11.9.13 + version: "16.x.x" repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: pgadmin4 - repository: file://charts/pgadmin4 # https://helm.runix.net - # License: https://github.com/rowanruseler/helm-charts/blob/main/LICENSE + repository: file://charts/pgadmin4 version: 1.19.0 condition: pgadmin4.enabled diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 3984b37f..c2ee872b 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -1,6 +1,6 @@
-# managed-identity-wallet +# Managed Identity Wallet - Verifiable Credential Revocation Service ![Version: 1.0.0-develop.4](https://img.shields.io/badge/Version-1.0.0--develop.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.4](https://img.shields.io/badge/AppVersion-1.0.0--develop.4-informational?style=flat-square) @@ -41,9 +41,7 @@ And at the same it shall support an uninterrupted tracking and tracing and docum ### Install Chart - helm install [RELEASE_NAME] tractusx-dev/managed-identity-wallet - - helm install [RELEASE_NAME] tractusx-stable/managed-identity-wallet + helm install [RELEASE_NAME] charts/managed-identity-wallet

(back to top)

@@ -75,124 +73,179 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document ## Requirements -| Repository | Name | Version | -|------------|------|---------| -| file://charts/pgadmin4 | pgadmin4 | 1.19.0 | -| https://charts.bitnami.com/bitnami | common | 2.x.x | -| https://charts.bitnami.com/bitnami | keycloak | 15.1.6 | +| Repository | Name | Version | +| ---------------------------------- | ---------- | ------- | +| file://charts/pgadmin4 | pgadmin4 | 1.19.0 | +| https://charts.bitnami.com/bitnami | common | 2.x.x | +| https://charts.bitnami.com/bitnami | keycloak | 15.1.6 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 |

(back to top)

## Values -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| affinity | object | `{}` | Affinity configuration | -| envs | object | `{}` | envs Parameters for the application (will be provided as environment variables) | -| extraVolumeMounts | list | `[]` | add volume mounts to the miw deployment | -| extraVolumes | list | `[]` | add volumes to the miw deployment | -| fullnameOverride | string | `""` | String to fully override common.names.fullname template | -| image.pullPolicy | string | `"Always"` | PullPolicy | -| image.repository | string | `"tractusx/managed-identity-wallet"` | Image repository | -| image.tag | string | `""` | Image tag (empty one will use "appVersion" value from chart definition) | -| ingress.annotations | object | `{}` | Ingress annotations | -| ingress.enabled | bool | `false` | Enable ingress controller resource | -| ingress.hosts | list | `[]` | Ingress accepted hostnames | -| ingress.tls | list | `[]` | Ingress TLS configuration | -| initContainers | list | `[]` | add initContainers to the miw deployment | -| keycloak.auth.adminPassword | string | `""` | Keycloak admin password | -| keycloak.auth.adminUser | string | `"admin"` | Keycloak admin user | -| keycloak.enabled | bool | `true` | Enable to deploy Keycloak | -| keycloak.extraEnvVars | list | `[]` | Extra environment variables | -| keycloak.ingress.annotations | object | `{}` | | -| keycloak.ingress.enabled | bool | `false` | | -| keycloak.ingress.hosts | list | `[]` | | -| keycloak.ingress.tls | list | `[]` | | -| keycloak.keycloakConfigCli.backoffLimit | int | `2` | Number of retries before considering a Job as failed | -| keycloak.keycloakConfigCli.enabled | bool | `true` | Enable to create the miw playground realm | -| keycloak.keycloakConfigCli.existingConfigmap | string | `"keycloak-realm-config"` | Existing configmap name for the realm configuration | -| keycloak.postgresql.auth.database | string | `"miw_keycloak"` | Database name | -| keycloak.postgresql.auth.password | string | `""` | KeycloakPostgresql password to set (if empty one is generated) | -| keycloak.postgresql.auth.username | string | `"miw_keycloak"` | Keycloak PostgreSQL user | -| keycloak.postgresql.enabled | bool | `true` | Enable to deploy PostgreSQL | -| keycloak.postgresql.nameOverride | string | `"keycloak-postgresql"` | Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. | -| livenessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":20,"periodSeconds":5,"timeoutSeconds":15}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | -| livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe at all | -| livenessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | -| livenessProbe.initialDelaySeconds | int | `20` | Number of seconds after the container has started before readiness probe are initiated. | -| livenessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | -| livenessProbe.timeoutSeconds | int | `15` | Number of seconds after which the probe times out. | -| miw.authorityWallet.bpn | string | `"BPNL000000000000"` | Authority Wallet BPNL | -| miw.authorityWallet.name | string | `""` | Authority Wallet Name | -| miw.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | -| miw.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | -| miw.database.encryptionKey.value | string | `""` | Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. | -| miw.database.host | string | `"{{ .Release.Name }}-postgresql"` | Database host | -| miw.database.name | string | `"miw_app"` | Database name | -| miw.database.port | int | `5432` | Database port | -| miw.database.secret | string | `"{{ .Release.Name }}-postgresql"` | Existing secret name for the database password | -| miw.database.secretPasswordKey | string | `"password"` | Existing secret key for the database password | -| miw.database.useSSL | bool | `false` | Set to true to enable SSL connection to the database | -| miw.database.user | string | `"miw"` | Database user | -| miw.environment | string | `"dev"` | Runtime environment. Should be ether local, dev, int or prod | -| miw.host | string | `"{{ .Release.Name }}-managed-identity-wallet:8080"` | Host name | -| miw.keycloak.clientId | string | `"miw_private_client"` | Keycloak client id | -| miw.keycloak.realm | string | `"miw_test"` | Keycloak realm | -| miw.keycloak.url | string | `"http://{{ .Release.Name }}-keycloak"` | Keycloak URL | -| miw.logging.level | string | `"INFO"` | Log level. Should be ether ERROR, WARN, INFO, DEBUG, or TRACE. | -| miw.ssi.enforceHttpsInDidWebResolution | bool | `true` | Enable to use HTTPS in DID Web Resolution | -| miw.ssi.vcExpiryDate | string | `""` | Verifiable Credential expiry date. Format 'dd-MM-yyyy'. If empty it is set to 31-12- | -| nameOverride | string | `""` | String to partially override common.names.fullname template (will maintain the release name) | -| networkPolicy.enabled | bool | `false` | If `true` network policy will be created to restrict access to managed-identity-wallet | -| networkPolicy.from | list | `[{"namespaceSelector":{}}]` | Specify from rule network policy for miw (defaults to all namespaces) | -| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector configuration | -| pgadmin4.enabled | bool | `false` | Enable to deploy pgAdmin | -| pgadmin4.env.email | string | `"admin@miw.com"` | Preset the admin user email | -| pgadmin4.env.password | string | `"very-secret-password"` | preset password (there is no auto-generated password) | -| pgadmin4.extraServerDefinitions.enabled | bool | `true` | enable the predefined server for pgadmin | -| pgadmin4.extraServerDefinitions.servers | object | `{}` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L84) how to configure the predefined servers | -| pgadmin4.ingress.annotations | object | `{}` | | -| pgadmin4.ingress.enabled | bool | `false` | Enagle pgAdmin ingress | -| pgadmin4.ingress.hosts | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L104) how to configure the ingress host(s) | -| pgadmin4.ingress.tls | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L109) how to configure tls for the ingress host(s) | -| podAnnotations | object | `{}` | PodAnnotation configuration | -| podSecurityContext | object | `{}` | PodSecurityContext | -| postgresql.auth.database | string | `"miw_app"` | Postgresql database to create | -| postgresql.auth.enablePostgresUser | bool | `false` | Enable postgresql admin user | -| postgresql.auth.password | string | `""` | Postgresql password to set (if empty one is generated) | -| postgresql.auth.postgresPassword | string | `""` | Postgresql admin user password | -| postgresql.auth.username | string | `"miw"` | Postgresql user to create | -| postgresql.backup.cronjob.schedule | string | `"* */6 * * *"` | Backup schedule | -| postgresql.backup.cronjob.storage.existingClaim | string | `""` | Name of an existing PVC to use | -| postgresql.backup.cronjob.storage.resourcePolicy | string | `"keep"` | Set resource policy to "keep" to avoid removing PVCs during a helm delete operation | -| postgresql.backup.cronjob.storage.size | string | `"8Gi"` | PVC Storage Request for the backup data volume | -| postgresql.backup.enabled | bool | `false` | Enable to create a backup cronjob | -| postgresql.enabled | bool | `true` | Enable to deploy Postgresql | -| readinessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":30,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":5}` | Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | -| readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe at all | -| readinessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | -| readinessProbe.initialDelaySeconds | int | `30` | Number of seconds after the container has started before readiness probe are initiated. | -| readinessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | -| readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed. | -| readinessProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | -| replicaCount | int | `1` | The amount of replicas to run | -| resources.limits.cpu | int | `2` | CPU resource limits | -| resources.limits.memory | string | `"1Gi"` | Memory resource limits | -| resources.requests.cpu | string | `"250m"` | CPU resource requests | -| resources.requests.memory | string | `"500Mi"` | Memory resource requests | -| secrets | object | `{}` | Parameters for the application (will be stored as secrets - so, for passwords, ...) | -| securityContext.allowPrivilegeEscalation | bool | `false` | Allow privilege escalation | -| securityContext.privileged | bool | `false` | Enable privileged container | -| securityContext.runAsGroup | int | `11111` | Group ID used to run the container | -| securityContext.runAsNonRoot | bool | `true` | Enable to run the container as a non-root user | -| securityContext.runAsUser | int | `11111` | User ID used to run the container | -| service.port | int | `8080` | Kubernetes Service port | -| service.type | string | `"ClusterIP"` | Kubernetes Service type | -| serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | -| serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | -| serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | -| tolerations | list | `[]` | Tolerations configuration | +| Key | Type | Default | Description | +| ------------------------------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| affinity | object | `{}` | Affinity configuration | +| envs | object | `{}` | envs Parameters for the application (will be provided as environment variables) | +| extraVolumeMounts | list | `[]` | add volume mounts to the miw deployment | +| extraVolumes | list | `[]` | add volumes to the miw deployment | +| fullnameOverride | string | `""` | String to fully override common.names.fullname template | +| image.pullPolicy | string | `"Always"` | PullPolicy | +| image.repository | string | `"tractusx/managed-identity-wallet"` | Image repository | +| image.tag | string | `""` | Image tag (empty one will use "appVersion" value from chart definition) | +| ingress.annotations | object | `{}` | Ingress annotations | +| ingress.enabled | bool | `false` | Enable ingress controller resource | +| ingress.hosts | list | `[]` | Ingress accepted hostnames | +| ingress.tls | list | `[]` | Ingress TLS configuration | +| initContainers | list | `[]` | add initContainers to the miw deployment | +| keycloak.auth.adminPassword | string | `""` | Keycloak admin password | +| keycloak.auth.adminUser | string | `"admin"` | Keycloak admin user | +| keycloak.enabled | bool | `true` | Enable to deploy Keycloak | +| keycloak.extraEnvVars | list | `[]` | Extra environment variables | +| keycloak.ingress.annotations | object | `{}` | | +| keycloak.ingress.enabled | bool | `false` | | +| keycloak.ingress.hosts | list | `[]` | | +| keycloak.ingress.tls | list | `[]` | | +| keycloak.keycloakConfigCli.backoffLimit | int | `2` | Number of retries before considering a Job as failed | +| keycloak.keycloakConfigCli.enabled | bool | `true` | Enable to create the miw playground realm | +| keycloak.keycloakConfigCli.existingConfigmap | string | `"keycloak-realm-config"` | Existing configmap name for the realm configuration | +| keycloak.postgresql.auth.database | string | `"miw_keycloak"` | Database name | +| keycloak.postgresql.auth.password | string | `""` | KeycloakPostgresql password to set (if empty one is generated) | +| keycloak.postgresql.auth.username | string | `"miw_keycloak"` | Keycloak PostgreSQL user | +| keycloak.postgresql.enabled | bool | `true` | Enable to deploy PostgreSQL | +| keycloak.postgresql.nameOverride | string | `"keycloak-postgresql"` | Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. | +| livenessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":20,"periodSeconds":5,"timeoutSeconds":15}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | +| livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe at all | +| livenessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | +| livenessProbe.initialDelaySeconds | int | `20` | Number of seconds after the container has started before readiness probe are initiated. | +| livenessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | +| livenessProbe.timeoutSeconds | int | `15` | Number of seconds after which the probe times out. | +| miw.authorityWallet.bpn | string | `"BPNL000000000000"` | Authority Wallet BPNL | +| miw.authorityWallet.name | string | `""` | Authority Wallet Name | +| miw.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | +| miw.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | +| miw.database.encryptionKey.value | string | `""` | Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. | +| miw.database.host | string | `"{{ .Release.Name }}-postgresql"` | Database host | +| miw.database.name | string | `"miw_app"` | Database name | +| miw.database.port | int | `5432` | Database port | +| miw.database.secret | string | `"{{ .Release.Name }}-postgresql"` | Existing secret name for the database password | +| miw.database.secretPasswordKey | string | `""` | Existing secret key for the database password | +| miw.database.useSSL | bool | `false` | Set to true to enable SSL connection to the database | +| miw.database.user | string | `"miw"` | Database user | +| miw.environment | string | `"dev"` | Runtime environment. Should be ether local, dev, int or prod | +| miw.host | string | `"{{ .Release.Name }}-managed-identity-wallet:8080"` | Host name | +| miw.keycloak.clientId | string | `"miw_private_client"` | Keycloak client id | +| miw.keycloak.realm | string | `"miw_test"` | Keycloak realm | +| miw.keycloak.url | string | `"http://{{ .Release.Name }}-keycloak"` | Keycloak URL | +| miw.logging.level | string | `"INFO"` | Log level. Should be ether ERROR, WARN, INFO, DEBUG, or TRACE. | +| miw.ssi.enforceHttpsInDidWebResolution | bool | `true` | Enable to use HTTPS in DID Web Resolution | +| miw.ssi.vcExpiryDate | string | `""` | Verifiable Credential expiry date. Format 'dd-MM-yyyy'. If empty it is set to 31-12- | +| nameOverride | string | `""` | String to partially override common.names.fullname template (will maintain the release name) | +| networkPolicy.enabled | bool | `false` | If `true` network policy will be created to restrict access to managed-identity-wallet | +| networkPolicy.from | list | `[{"namespaceSelector":{}}]` | Specify from rule network policy for miw (defaults to all namespaces) | +| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector configuration | +| pgadmin4.enabled | bool | `false` | Enable to deploy pgAdmin | +| pgadmin4.env.email | string | `"admin@miw.com"` | Preset the admin user email | +| pgadmin4.env.password | string | `"very-secret-password"` | preset password (there is no auto-generated password) | +| pgadmin4.extraServerDefinitions.enabled | bool | `true` | enable the predefined server for pgadmin | +| pgadmin4.extraServerDefinitions.servers | object | `{}` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L84) how to configure the predefined servers | +| pgadmin4.ingress.annotations | object | `{}` | | +| pgadmin4.ingress.enabled | bool | `false` | Enagle pgAdmin ingress | +| pgadmin4.ingress.hosts | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L104) how to configure the ingress host(s) | +| pgadmin4.ingress.tls | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L109) how to configure tls for the ingress host(s) | +| podAnnotations | object | `{}` | PodAnnotation configuration | +| podSecurityContext | object | `{}` | PodSecurityContext | +| postgresql.auth.database | string | `"miw_app"` | Postgresql database to create | +| postgresql.auth.enablePostgresUser | bool | `false` | Enable postgresql admin user | +| postgresql.auth.password | string | `""` | Postgresql password to set (if empty one is generated) | +| postgresql.auth.postgresPassword | string | `""` | Postgresql admin user password | +| postgresql.auth.username | string | `"miw"` | Postgresql user to create | +| postgresql.backup.cronjob.schedule | string | `"* */6 * * *"` | Backup schedule | +| postgresql.backup.cronjob.storage.existingClaim | string | `""` | Name of an existing PVC to use | +| postgresql.backup.cronjob.storage.resourcePolicy | string | `"keep"` | Set resource policy to "keep" to avoid removing PVCs during a helm delete operation | +| postgresql.backup.cronjob.storage.size | string | `"8Gi"` | PVC Storage Request for the backup data volume | +| postgresql.backup.enabled | bool | `false` | Enable to create a backup cronjob | +| postgresql.enabled | bool | `true` | Enable to deploy Postgresql | +| readinessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":30,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":5}` | Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | +| readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe at all | +| readinessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | +| readinessProbe.initialDelaySeconds | int | `30` | Number of seconds after the container has started before readiness probe are initiated. | +| readinessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | +| readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed. | +| readinessProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | +| replicaCount | int | `1` | The amount of replicas to run | +| resources.limits.cpu | int | `2` | CPU resource limits | +| resources.limits.memory | string | `"1Gi"` | Memory resource limits | +| resources.requests.cpu | string | `"250m"` | CPU resource requests | +| resources.requests.memory | string | `"500Mi"` | Memory resource requests | +| secrets | object | `{}` | Parameters for the application (will be stored as secrets - so, for passwords, ...) | +| securityContext.allowPrivilegeEscalation | bool | `false` | Allow privilege escalation | +| securityContext.privileged | bool | `false` | Enable privileged container | +| securityContext.runAsGroup | int | `11111` | Group ID used to run the container | +| securityContext.runAsNonRoot | bool | `true` | Enable to run the container as a non-root user | +| securityContext.runAsUser | int | `11111` | User ID used to run the container | +| service.port | int | `8080` | Kubernetes Service port | +| service.type | string | `"ClusterIP"` | Kubernetes Service type | +| serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | +| serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | +| serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | +| tolerations | list | `[]` | Tolerations configuration | +| vcrs.replicaCount | int | `1` | Number of replicas to run | +| vcrs.url | string | `"https://a888-203-129-213-107.ngrok-free.app"` | Application URL | +| vcrs.vcContexts | string | `"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"` | App VC context | +| vcrs.domain.url | string | `"https://977d-203-129-213-107.ngrok-free.app"` | App domain | +| vcrs.domain.host | string | `"localhost"` | The application name | +| vcrs.appName | string | `"verifiable-credential-revocation-service"` | The configmap name | +| vcrs.appPort | string | `"8081"` | The application port | +| vcrs.appProfile | string | `"local"` | The application profile | +| vcrs.applicationLogLevel | string | `"DEBUG"` | The application log level | +| vcrs.configName | string | `"verifiable-credential-revocation-service-config"` | The service name | +| vcrs.serviceName | string | `"verifiable-credential-revocation-service"` | The secret name | +| vcrs.secretName | string | `"verifiable-credential-revocation-service-secret"` | The secret name | +| vcrs.ingressName | string | `"verifiable-credential-revocation-service-ingress"` | Ingress name | +| vcrs.image.repository | string | `"docker.io/example"` | Image repository | +| vcrs.image.pullPolicy | string | `"IfNotPresent"` | PullPolicy | +| vcrs.image.tag | string | `"latest"` | Image tag (empty one will use "appVersion" value from chart definition) | +| vcrs.resources.requests.cpu | string | `"250m"` | CPU resource requests | +| vcrs.resources.requests.memory | string | `"512Mi"` | Memory resource requests | +| vcrs.resources.limits.cpu | string | `"500m"` | CPU resource limits | +| vcrs.resources.limits.memory | string | `"1Gi"` | Memory resource limits | +| vcrs.livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe | +| vcrs.livenessProbe.failureThreshold | int | `5` | Failure threshold for liveness probe | +| vcrs.livenessProbe.initialDelaySeconds | int | `60` | Initial delay before liveness probe starts | +| vcrs.livenessProbe.timeoutSeconds | int | `30` | Timeout for liveness probe | +| vcrs.livenessProbe.periodSeconds | int | `15` | How often to perform liveness probe | +| vcrs.readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe | +| vcrs.readinessProbe.failureThreshold | int | `5` | Failure threshold for readiness probe | +| vcrs.readinessProbe.initialDelaySeconds | int | `60` | Initial delay before readiness probe starts | +| vcrs.readinessProbe.timeoutSeconds | int | `15` | Timeout for readiness probe | +| vcrs.readinessProbe.periodSeconds | int | `15` | How often to perform readiness probe | +| vcrs.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the readiness probe to be considered successful | +| vcrs.ingress.enabled | bool | `false` | Enable to deploy ingress | +| vcrs.ingress.tls | bool | `false` | TLS configuration for ingress | +| vcrs.ingress.urlPrefix | string | `/` | URL prefix for ingress | +| vcrs.ingress.className | string | `"nginx"` | Ingress class name | +| vcrs.ingress.annotations | object | `{}` | Ingress annotations | +| vcrs.ingress.service.type | string | `"ClusterIP"` | Kubernetes Service type | +| vcrs.ingress.service.port | int | `8081` | Kubernetes Service port | +| vcrs.database.databaseHost | string | `"managed-identity-wallet-postgresql"` | The Database Host | +| vcrs.database.databasePort | int | `5432` | The Database Port | +| vcrs.database.databaseName | string | `"vcrs_app"` | The Database Name | +| vcrs.database.databaseUseSSL | bool | `false` | The Database SSL | +| vcrs.database.databaseUsername | string | `"vcrs"` | The Database Username | +| vcrs.database.databaseConnectionPoolSize | int | `10` | The Database connection pool size | +| vcrs.database.databasepass | string | `""` | The Database password | +| vcrs.swagger.enableSwaggerUi | bool | `true` | Enable Swagger UI | +| vcrs.swagger.enableApiDoc | bool | `true` | Enable Swagger API Doc | +| vcrs.security.serviceSecurityEnabed | bool | `true` | Enable application security | +| vcrs.keycloak.enabled | bool | `false` | Enable Keycloak | +| vcrs.keycloak.keycloakRealm | string | `"miw_test"` | Keycloak Realm | +| vcrs.keycloak.clientId | string | `"miw_private_client"` | Keycloak Client ID | +| vcrs.keycloak.publicClientId | string | `"miw_public_client"` | Keycloak Public Client ID | +| vcrs.keycloak.authServerUrl | string | `"http://{{ .Release.Name }}-keycloak"` | Keycloak Auth Server URL | +| vcrs.logging.revocation | string | `"INFO"` | Logging method for revocation | + For more information on how to configure the Keycloak see - https://github.com/bitnami/charts/tree/main/bitnami/keycloak. @@ -260,9 +313,10 @@ when deploying the MIW in a production environment: ## Maintainers -| Name | Email | Url | -| ---- | ------ | --- | +| Name | Email | Url | +| -------------- | ---------------------------------- | ---------------------------------- | | Dominik Pinsel | | | +| Rohit Solanki | | |

(back to top)

diff --git a/charts/managed-identity-wallet/templates/NOTES.txt b/charts/managed-identity-wallet/templates/NOTES.txt index ddfc099c..2c3e36f9 100644 --- a/charts/managed-identity-wallet/templates/NOTES.txt +++ b/charts/managed-identity-wallet/templates/NOTES.txt @@ -17,6 +17,7 @@ {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "managed-identity-wallet.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT + Visit http://127.0.0.1:8080 (MIW) and http://127.0.0.1:8081 (VCRS) to use your application + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:8080 + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8081:8081 {{- end }} diff --git a/charts/managed-identity-wallet/templates/_helpers.tpl b/charts/managed-identity-wallet/templates/_helpers.tpl index cf153767..6fd0b239 100644 --- a/charts/managed-identity-wallet/templates/_helpers.tpl +++ b/charts/managed-identity-wallet/templates/_helpers.tpl @@ -24,6 +24,10 @@ Expand the name of the chart. {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} +{{- define "verifiable-credential-revocation-service.name" -}} +{{- default .Chart.Name .Values.vcrs.env.APPLICATION_NAME | 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). @@ -42,6 +46,19 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} +{{- define "verifiable-credential-revocation-service.fullname" -}} +{{- if .Values.vcrs.env.APPLICATION_NAME }} +{{- .Values.vcrs.env.APPLICATION_NAME | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.vcrs.env.APPLICATION_NAME }} +{{- 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. */}} @@ -49,6 +66,10 @@ Create chart name and version as used by the chart label. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} +{{- define "verifiable-credential-revocation-service.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* Common labels */}} @@ -61,6 +82,15 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} +{{- define "verifiable-credential-revocation-service.labels" -}} +helm.sh/chart: {{ include "verifiable-credential-revocation-service.chart" . }} +{{ include "verifiable-credential-revocation-service.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + {{/* Selector labels */}} @@ -69,6 +99,11 @@ app.kubernetes.io/name: {{ include "managed-identity-wallet.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "verifiable-credential-revocation-service.selectorLabels" -}} +app.kubernetes.io/name: {{ include "verifiable-credential-revocation-service.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + {{/* Create the name of the service account to use */}} diff --git a/charts/managed-identity-wallet/templates/deployment.yaml b/charts/managed-identity-wallet/templates/miw-deployment.yaml similarity index 96% rename from charts/managed-identity-wallet/templates/deployment.yaml rename to charts/managed-identity-wallet/templates/miw-deployment.yaml index 801dbf9a..805b6c09 100644 --- a/charts/managed-identity-wallet/templates/deployment.yaml +++ b/charts/managed-identity-wallet/templates/miw-deployment.yaml @@ -1,4 +1,4 @@ -# /******************************************************************************** +# ******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +# ******************************************************************************** apiVersion: apps/v1 kind: Deployment @@ -117,7 +117,7 @@ spec: - name: http containerPort: 8080 protocol: TCP - {{- with .Values.livenessProbe }} + {{- with .Values.miw.livenessProbe }} {{- if .enabled }} livenessProbe: httpGet: @@ -130,7 +130,7 @@ spec: timeoutSeconds: {{ .timeoutSeconds }} {{- end }} {{- end }} - {{- with .Values.readinessProbe }} + {{- with .Values.miw.readinessProbe }} {{- if .enabled }} readinessProbe: httpGet: @@ -162,4 +162,4 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} volumes: - {{- toYaml .Values.extraVolumes | nindent 8 }} + {{- toYaml .Values.extraVolumes | nindent 8 }} \ No newline at end of file diff --git a/charts/managed-identity-wallet/templates/ingress.yaml b/charts/managed-identity-wallet/templates/miw-ingress.yaml similarity index 96% rename from charts/managed-identity-wallet/templates/ingress.yaml rename to charts/managed-identity-wallet/templates/miw-ingress.yaml index a550fece..afb584a8 100644 --- a/charts/managed-identity-wallet/templates/ingress.yaml +++ b/charts/managed-identity-wallet/templates/miw-ingress.yaml @@ -1,4 +1,4 @@ -# /******************************************************************************** +# ******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +# ******************************************************************************** {{ if .Values.ingress.enabled -}} {{- $fullName := include "managed-identity-wallet.fullname" . -}} diff --git a/charts/managed-identity-wallet/templates/secret.yaml b/charts/managed-identity-wallet/templates/miw-secret.yaml similarity index 94% rename from charts/managed-identity-wallet/templates/secret.yaml rename to charts/managed-identity-wallet/templates/miw-secret.yaml index 832ecf87..ff3af397 100644 --- a/charts/managed-identity-wallet/templates/secret.yaml +++ b/charts/managed-identity-wallet/templates/miw-secret.yaml @@ -1,4 +1,4 @@ -# /******************************************************************************** +# ******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +# ******************************************************************************** {{ if .Values.secrets -}} apiVersion: v1 diff --git a/charts/managed-identity-wallet/templates/service.yaml b/charts/managed-identity-wallet/templates/miw-service.yaml similarity index 92% rename from charts/managed-identity-wallet/templates/service.yaml rename to charts/managed-identity-wallet/templates/miw-service.yaml index 8c067a45..4dd6103d 100644 --- a/charts/managed-identity-wallet/templates/service.yaml +++ b/charts/managed-identity-wallet/templates/miw-service.yaml @@ -1,4 +1,4 @@ -# /******************************************************************************** +# ******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +# ******************************************************************************** apiVersion: v1 kind: Service diff --git a/charts/managed-identity-wallet/templates/networkpolicy.yaml b/charts/managed-identity-wallet/templates/networkpolicy.yaml index f989b9b7..7fa2a38d 100644 --- a/charts/managed-identity-wallet/templates/networkpolicy.yaml +++ b/charts/managed-identity-wallet/templates/networkpolicy.yaml @@ -1,4 +1,4 @@ -# /******************************************************************************** +# ******************************************************************************** # * Copyright (c) 2024 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +# ******************************************************************************** {{- if .Values.networkPolicy.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy diff --git a/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml b/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml index 53fd2be4..35117541 100644 --- a/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml +++ b/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml @@ -21,7 +21,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ .Release.Name }}-pgadmin4-server-definitions + name: pgadmin4-server-definitions labels: {{- include "pgadmin.labels" . | nindent 4 }} data: diff --git a/charts/managed-identity-wallet/templates/psql-pv.yaml b/charts/managed-identity-wallet/templates/psql-pv.yaml new file mode 100644 index 00000000..828a4e87 --- /dev/null +++ b/charts/managed-identity-wallet/templates/psql-pv.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-seed-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: standard \ No newline at end of file diff --git a/charts/managed-identity-wallet/templates/vcrs-configmap.yaml b/charts/managed-identity-wallet/templates/vcrs-configmap.yaml new file mode 100644 index 00000000..bf07ec84 --- /dev/null +++ b/charts/managed-identity-wallet/templates/vcrs-configmap.yaml @@ -0,0 +1,27 @@ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "verifiable-credential-revocation-service.fullname" . }} +data: + {{- range $key, $val := .Values.vcrs.env }} + {{ $key }}: {{ $val | quote }} + {{- end}} \ No newline at end of file diff --git a/charts/managed-identity-wallet/templates/vcrs-deployment.yaml b/charts/managed-identity-wallet/templates/vcrs-deployment.yaml new file mode 100644 index 00000000..95db61ce --- /dev/null +++ b/charts/managed-identity-wallet/templates/vcrs-deployment.yaml @@ -0,0 +1,83 @@ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "verifiable-credential-revocation-service.fullname" . }} + labels: + {{- include "verifiable-credential-revocation-service.labels" . | nindent 4 }} +spec: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + {{- include "verifiable-credential-revocation-service.selectorLabels" . | nindent 6 }} + replicas: {{ .Values.vcrs.replicaCount }} + revisionHistoryLimit: 2 + template: + metadata: + labels: + {{- include "verifiable-credential-revocation-service.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: {{ include "verifiable-credential-revocation-service.fullname" . }} + image: {{ .Values.vcrs.image.repository }}:{{ default .Chart.AppVersion .Values.vcrs.image.tag }} + imagePullPolicy: {{ .Values.vcrs.image.pullPolicy }} + resources: + {{- toYaml .Values.vcrs.resources | nindent 12 }} + envFrom: + - secretRef: + name: {{ .Values.vcrs.secretName }} + - configMapRef: + name: {{ .Values.vcrs.configName }} + {{- with .Values.vcrs.livenessProbe }} + {{- if .enabled }} + ports: + - name: http + containerPort: 8081 + protocol: TCP + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8081 + scheme: HTTP + failureThreshold: {{ .failureThreshold }} + initialDelaySeconds: {{ .initialDelaySeconds }} + periodSeconds: {{ .periodSeconds }} + timeoutSeconds: {{ .timeoutSeconds }} + {{- end }} + {{- end }} + {{- with .Values.vcrs.readinessProbe }} + {{- if .enabled }} + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8081 + scheme: HTTP + failureThreshold: {{ .failureThreshold }} + initialDelaySeconds: {{ .initialDelaySeconds }} + periodSeconds: {{ .periodSeconds }} + successThreshold: {{ .successThreshold }} + timeoutSeconds: {{ .timeoutSeconds }} + {{- end }} + {{- end }} \ No newline at end of file diff --git a/charts/managed-identity-wallet/templates/vcrs-ingress.yaml b/charts/managed-identity-wallet/templates/vcrs-ingress.yaml new file mode 100644 index 00000000..0f36912b --- /dev/null +++ b/charts/managed-identity-wallet/templates/vcrs-ingress.yaml @@ -0,0 +1,80 @@ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + +{{- if .Values.vcrs.ingress.enabled -}} +{{- $fullName := include "verifiable-credential-revocation-service.fullname" . -}} +{{- $svcPort := .Values.vcrs.ingress.service.port -}} +{{- if and .Values.ingress.vcrs.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.vcrs.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.vcrs.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.vcrs.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "verifiable-credential-revocation-service.labels" . | nindent 4 }} + {{- with .Values.ingress.vcrs.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.vcrs.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.vcrs.className }} + {{- end }} + {{- if .Values.ingress.vcrs.tls }} + tls: + {{- range .Values.ingress.vcrs.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.vcrs.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/managed-identity-wallet/templates/vcrs-secrets.yaml b/charts/managed-identity-wallet/templates/vcrs-secrets.yaml new file mode 100644 index 00000000..82f8d7fd --- /dev/null +++ b/charts/managed-identity-wallet/templates/vcrs-secrets.yaml @@ -0,0 +1,27 @@ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "verifiable-credential-revocation-service.fullname" . }} +type: Opaque +data: + {{- range $key, $val := .Values.vcrs.secrets }} + {{ $key }}: {{ $val | b64enc }} + {{- end}} diff --git a/charts/managed-identity-wallet/templates/vcrs-service.yaml b/charts/managed-identity-wallet/templates/vcrs-service.yaml new file mode 100644 index 00000000..15412d48 --- /dev/null +++ b/charts/managed-identity-wallet/templates/vcrs-service.yaml @@ -0,0 +1,32 @@ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "verifiable-credential-revocation-service.fullname" . }} +spec: + type: {{ .Values.vcrs.ingress.service.type }} + ports: + - port: {{ .Values.vcrs.ingress.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "verifiable-credential-revocation-service.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 0b87fe37..8f627ff0 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -1,30 +1,30 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ - +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### +# +# ----------------------------------------------- Values for Managed Identity Wallet ----------------------------------------------- # +# # -- The amount of replicas to run replicaCount: 1 - # -- String to partially override common.names.fullname template (will maintain the release name) nameOverride: "" # -- String to fully override common.names.fullname template fullnameOverride: "" - image: # -- Image repository repository: tractusx/managed-identity-wallet @@ -32,14 +32,11 @@ image: pullPolicy: Always # -- Image tag (empty one will use "appVersion" value from chart definition) tag: "" - - +imagePullSecrets: [] # -- Parameters for the application (will be stored as secrets - so, for passwords, ...) secrets: {} - # -- envs Parameters for the application (will be provided as environment variables) envs: {} - serviceAccount: # -- Enable creation of ServiceAccount create: true @@ -47,13 +44,12 @@ serviceAccount: annotations: {} # -- The name of the ServiceAccount to use. name: "" - service: # -- Kubernetes Service type type: ClusterIP # -- Kubernetes Service port port: 8080 - +# -- Ingress Configuration ingress: # -- Enable ingress controller resource enabled: false @@ -70,10 +66,10 @@ ingress: # - secretName: chart-example-tls # hosts: # - chart-example.local - -# -- PodSecurityContext + className: nginx +# -- Pod security configurations podSecurityContext: {} - +# -- Pod security parameters securityContext: # -- Enable privileged container privileged: false @@ -85,35 +81,8 @@ securityContext: runAsGroup: 11111 # -- Enable to run the container as a non-root user runAsNonRoot: true - -# -- Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) -livenessProbe: - # -- Enables/Disables the livenessProbe at all - enabled: true - # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. - failureThreshold: 3 - # -- Number of seconds after the container has started before readiness probe are initiated. - initialDelaySeconds: 20 - # -- Number of seconds after which the probe times out. - timeoutSeconds: 15 - # -- How often (in seconds) to perform the probe - periodSeconds: 5 - -# -- Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) -readinessProbe: - # -- Enables/Disables the readinessProbe at all - enabled: true - # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. - failureThreshold: 3 - # -- Number of seconds after the container has started before readiness probe are initiated. - initialDelaySeconds: 30 - # -- How often (in seconds) to perform the probe - periodSeconds: 5 - # -- Minimum consecutive successes for the probe to be considered successful after having failed. - successThreshold: 1 - # -- Number of seconds after which the probe times out. - timeoutSeconds: 5 - + # # -- Filesystem group ID + # fsGroup: 1001 resources: requests: # -- CPU resource requests @@ -125,38 +94,29 @@ resources: cpu: 2 # -- Memory resource limits memory: 1Gi - # -- NodeSelector configuration nodeSelector: "kubernetes.io/os": linux - # -- Tolerations configuration tolerations: [] - # -- Affinity configuration affinity: {} - # -- PodAnnotation configuration podAnnotations: {} - # -- add initContainers to the miw deployment initContainers: [] - networkPolicy: # -- If `true` network policy will be created to restrict access to managed-identity-wallet enabled: false # -- Specify from rule network policy for miw (defaults to all namespaces) from: - - namespaceSelector: {} - + - namespaceSelector: {} # -- add volumes to the miw deployment extraVolumes: [] - -# -- add volume mounts to the miw deployment extraVolumeMounts: [] - -## @section Managed Identity Wallet Primary Parameters -## +# +# -----------------------------------------------MIW----------------------------------------------- # +# miw: ## @param miw.host Host name ## @param miw.logging.level Log level. Should be ether ERROR, WARN, INFO, DEBUG, or TRACE. @@ -184,15 +144,16 @@ miw: # -- Database port port: 5432 # -- Database host - host: "{{ .Release.Name }}-postgresql" + host: "managed-identity-wallet-postgresql" # -- Database user user: "miw" # -- Database name name: "miw_app" # -- Existing secret name for the database password - secret: "{{ .Release.Name }}-postgresql" + secret: "managed-identity-wallet-postgresql" # -- Existing secret key for the database password secretPasswordKey: "password" + # -- Password encryption configuratons encryptionKey: # -- Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. value: "" @@ -207,32 +168,71 @@ miw: clientId: "miw_private_client" # -- Keycloak URL url: "http://{{ .Release.Name }}-keycloak" - -# For more information on how to configure the Keycloak chart see https://github.com/bitnami/charts/tree/main/bitnami/keycloak. + # -- Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + livenessProbe: + # -- Enables/Disables the livenessProbe at all + enabled: true + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. + failureThreshold: 3 + # -- Number of seconds after the container has started before readiness probe are initiated. + initialDelaySeconds: 20 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 15 + # -- How often (in seconds) to perform the probe + periodSeconds: 5 + # -- Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + readinessProbe: + # -- Enables/Disables the readinessProbe at all + enabled: true + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. + failureThreshold: 3 + # -- Number of seconds after the container has started before readiness probe are initiated. + initialDelaySeconds: 30 + # -- How often (in seconds) to perform the probe + periodSeconds: 5 + # -- Minimum consecutive successes for the probe to be considered successful after having failed. + successThreshold: 1 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 5 + # For more information on how to configure the Keycloak chart see https://github.com/bitnami/charts/tree/main/bitnami/keycloak. +# ----------------------------------------------- KEYCLOAK ----------------------------------------------- # keycloak: # -- Enable to deploy Keycloak enabled: true # -- Extra environment variables extraEnvVars: [] - # - name: KEYCLOAK_HOSTNAME - # value: "{{ .Release.Name }}-keycloak" + # - name: KEYCLOAK_HOSTNAME + # value: "keycloak" postgresql: # -- Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. nameOverride: "keycloak-postgresql" # -- Enable to deploy PostgreSQL enabled: true auth: - # -- Keycloak PostgreSQL user + # -- Postgresql admin user password username: "miw_keycloak" # -- KeycloakPostgresql password to set (if empty one is generated) - password: "" + password: "adminpass" # -- Database name database: "miw_keycloak" + volumePermissions: + enabled: true ingress: + # -- Enable ingress controller resource enabled: false + # -- Ingress annotations annotations: {} + # -- Ingress accepted hostnames hosts: [] + # - host: chart-example.local + # paths: + # - path: / + # pathType: Prefix + # -- Ingress TLS configuration tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local auth: # -- Keycloak admin user adminUser: "admin" @@ -245,28 +245,53 @@ keycloak: existingConfigmap: keycloak-realm-config # -- Number of retries before considering a Job as failed backoffLimit: 2 - +# ----------------------------------------------- POSTGRESQL ----------------------------------------------- # # For more information on how to configure the PostgreSQL chart see https://github.com/bitnami/charts/tree/main/bitnami/postgresql. postgresql: # -- Enable to deploy Postgresql enabled: true + image: + tag: "16-debian-12" + # -- Debug logs + debug: true auth: # -- Enable postgresql admin user - enablePostgresUser: false + enablePostgresUser: true # -- Postgresql admin user password - postgresPassword: "" + postgresPassword: "adminpass" # -- Postgresql user to create username: "miw" # -- Postgresql password to set (if empty one is generated) - password: "" + password: "adminpass" # -- Postgresql database to create database: "miw_app" + # -- Creating a new database for VCRS application (Edit the DB configurations as required in configmap) + primary: + extraVolumes: + - name: postgres-seed + persistentVolumeClaim: + claimName: postgres-seed-pvc + extraVolumeMounts: + - mountPath: /docker-entrypoint-initdb.d/seed + name: postgres-seed + initdb: + user: "postgres" + password: "adminpass" + scripts: + init.sql: | + CREATE DATABASE vcrs_app; + CREATE USER vcrs WITH ENCRYPTED PASSWORD 'adminpass'; + GRANT ALL PRIVILEGES ON DATABASE vcrs_app TO vcrs; + \c vcrs_app + GRANT ALL ON SCHEMA public TO vcrs; backup: # -- Enable to create a backup cronjob enabled: false + #Cronjob Configuration cronjob: # -- Backup schedule schedule: "* */6 * * *" + # Backup Storage configuration storage: # -- Name of an existing PVC to use existingClaim: "" @@ -274,7 +299,9 @@ postgresql: resourcePolicy: "keep" # -- PVC Storage Request for the backup data volume size: "8Gi" - + volumePermissions: + enabled: true +# ----------------------------------------------- PGADMIN ----------------------------------------------- # # For more information on how to configure the pgadmin chart see https://artifacthub.io/packages/helm/runix/pgadmin4. # (Here we're using a stripped-down version of the pgadmin chart, to just ) pgadmin4: @@ -318,3 +345,138 @@ pgadmin4: subPath: servers.json mountPath: "/pgadmin4/servers.json" readOnly: true +# +# ----------------------------------------------- Values for Verifiable Credential Revocation Service application ----------------------------------------------- # +# +vcrs: + replicaCount: 1 + # -- Revocation application configuration + host: localhost + # -- The configmap name + nameOverride: "verifiable-credential-revocation-service" + # -- String to partially override common.names.fullname template (will maintain the release name) + fullnameOverride: "verifiable-credential-revocation-service" + # -- ConfigMap Name + configName: "verifiable-credential-revocation-service-config" + # -- The Service name + serviceName: "verifiable-credential-revocation-service" + # -- The Secret name + secretName: "verifiable-credential-revocation-service-secret" + image: + # -- Image repository + repository: public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service + # -- PullPolicy + pullPolicy: IfNotPresent + # -- Image tag (empty one will use "appVersion" value from chart definition) + tag: "latest" + env: + # -- The application name + APPLICATION_NAME: verifiable-credential-revocation-service + # -- The application port + APPLICATION_PORT: 8081 + # -- The application profile + APPLICATION_PROFILE: local + # -- The Database Host + DATABASE_HOST: managed-identity-wallet-postgresql + # -- The Database Port + DATABASE_PORT: 5432 + # -- The Database Name + DATABASE_NAME: vcrs_app + # -- The Database SSL + DATABASE_USE_SSL_COMMUNICATION: false + # -- The Database Name + DATABASE_USERNAME: vcrs + # -- The Database connection pool size + DATABASE_CONNECTION_POOL_SIZE: 10 + # -- Swagger UI config + ENABLE_SWAGGER_UI: true + # -- Swagger Api Doc + ENABLE_API_DOC: true + # -- The application log level + APPLICATION_LOG_LEVEL: DEBUG + # Enable application security + SERVICE_SECURITY_ENABLED: true + # -- KeyClocak Configurations + KEYCLOAK_REALM: miw_test + # -- ClientID Config + KEYCLOAK_CLIENT_ID: miw_private_client + # -- ClientID Config + KEYCLOAK_PUBLIC_CLIENT_ID: miw_public_client + # -- Auth URL for Keycloak + AUTH_SERVER_URL: "http://{{ .Release.Name }}-keycloak" + # -- Revocation application configuration + MIW_URL: https://a888-203-129-213-107.ngrok-free.app + VC_SCHEMA_LINK: https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json + DOMAIN_URL: https://977d-203-129-213-107.ngrok-free.app + # Application logging configurations + APP_LOG_LEVEL: INFO + secrets: + # -- The Database Password + DATABASE_PASSWORD: "adminpass" + resources: + requests: + # -- CPU resource requests + cpu: 250m + # -- Memory resource requests + memory: 512Mi + limits: + # -- CPU resource limits + cpu: 500m + # -- Memory resource limits + memory: 1Gi + # -- Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + livenessProbe: + # -- Enables/Disables the livenessProbe at all + enabled: true + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. + failureThreshold: 5 + # -- Number of seconds after the container has started before readiness probes are initiated. + initialDelaySeconds: 60 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 30 + # -- How often (in seconds) to perform the probe + periodSeconds: 15 + # -- Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + readinessProbe: + # -- Enables/Disables the readinessProbe at all + enabled: true + # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. + failureThreshold: 5 + # -- Number of seconds after the container has started before readiness probe are initiated. + initialDelaySeconds: 60 + # -- How often (in seconds) to perform the probe + periodSeconds: 15 + # -- Minimum consecutive successes for the probe to be considered successful after having failed. + successThreshold: 1 + # -- Number of seconds after which the probe times out. + timeoutSeconds: 15 + # -- ingress configuration + ingressName: "verifiable-credential-revocation-service-ingress" + ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + service: + # -- Kubernetes Service type + type: ClusterIP + # -- Kubernetes Service port + port: 8081 + database: + encryptionKey: + # -- Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. + value: "" + # -- Existing secret for database encryption key + secret: "" + # -- Existing secret key for database encryption key + secretKey: "" From 82c399da6b57562c57774a13cbdb59484eaec4d3 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Fri, 23 Aug 2024 10:29:36 +0530 Subject: [PATCH 44/60] docs: code changes reverted --- charts/managed-identity-wallet/Chart.lock | 28 +++++----- charts/managed-identity-wallet/Chart.yaml | 8 +-- charts/managed-identity-wallet/README.md | 2 + .../templates/miw-deployment.yaml | 4 +- .../templates/miw-ingress.yaml | 4 +- .../templates/miw-secret.yaml | 4 +- .../templates/miw-service.yaml | 4 +- .../templates/networkpolicy.yaml | 7 +-- .../templates/pgAdmin-server-definitions.yaml | 2 +- .../templates/psql-pv.yaml | 19 +++++++ .../templates/serviceaccount.yaml | 36 ++++++------- .../templates/vcrs-ingress.yaml | 29 +++++----- charts/managed-identity-wallet/values.yaml | 53 +++++++++---------- 13 files changed, 110 insertions(+), 90 deletions(-) diff --git a/charts/managed-identity-wallet/Chart.lock b/charts/managed-identity-wallet/Chart.lock index 5f15f8b4..2fd40018 100644 --- a/charts/managed-identity-wallet/Chart.lock +++ b/charts/managed-identity-wallet/Chart.lock @@ -1,15 +1,15 @@ dependencies: - - name: keycloak - repository: https://charts.bitnami.com/bitnami - version: 22.1.0 - - name: common - repository: https://charts.bitnami.com/bitnami - version: 2.13.3 - - name: postgresql - repository: https://charts.bitnami.com/bitnami - version: 11.9.13 - - name: pgadmin4 - repository: file://charts/pgadmin4 - version: 1.19.0 -digest: sha256:fb94864221b4fed31894b48ac56b72a2324da0dc1cb1d6888cc52c3490685df7 -generated: "2023-12-15T10:30:41.880265+01:00" +- name: keycloak + repository: https://charts.bitnami.com/bitnami + version: 15.1.6 +- name: common + repository: https://charts.bitnami.com/bitnami + version: 2.22.0 +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 11.9.13 +- name: pgadmin4 + repository: file://charts/pgadmin4 + version: 1.19.0 +digest: sha256:886b90f763f2320a1601e15b06264065a764f51fc34d592c0f0a08bd76f01635 +generated: "2024-08-22T18:04:25.649769241+05:30" diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index d782405a..809b7a64 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -27,7 +27,6 @@ type: application version: 1.0.0-develop.4 appVersion: 1.0.0-develop.4 -home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: - Managed Identity Wallet - eclipse-tractusx @@ -39,7 +38,7 @@ maintainers: url: https://github.com/DominikPinsel dependencies: - name: keycloak - version: 22.1.0 + version: 15.1.6 repository: https://charts.bitnami.com/bitnami condition: keycloak.enabled - name: common @@ -48,10 +47,11 @@ dependencies: - bitnami-common version: 2.x.x - name: postgresql - version: "16.x.x" + version: 11.9.13 repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: pgadmin4 - repository: file://charts/pgadmin4 + repository: file://charts/pgadmin4 # https://helm.runix.net + # License: https://github.com/rowanruseler/helm-charts/blob/main/LICENSE version: 1.19.0 condition: pgadmin4.enabled diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index c2ee872b..3228e7d3 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -43,6 +43,8 @@ And at the same it shall support an uninterrupted tracking and tracing and docum helm install [RELEASE_NAME] charts/managed-identity-wallet + #This will spin up the container for MIW application, VSRS application, KeyCloak and Postgresql +

(back to top)

The command deploys miw on the Kubernetes cluster in the default configuration. diff --git a/charts/managed-identity-wallet/templates/miw-deployment.yaml b/charts/managed-identity-wallet/templates/miw-deployment.yaml index 805b6c09..0e147aa1 100644 --- a/charts/managed-identity-wallet/templates/miw-deployment.yaml +++ b/charts/managed-identity-wallet/templates/miw-deployment.yaml @@ -1,4 +1,4 @@ -# ******************************************************************************** +# /******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************** +# ********************************************************************************/ apiVersion: apps/v1 kind: Deployment diff --git a/charts/managed-identity-wallet/templates/miw-ingress.yaml b/charts/managed-identity-wallet/templates/miw-ingress.yaml index afb584a8..a550fece 100644 --- a/charts/managed-identity-wallet/templates/miw-ingress.yaml +++ b/charts/managed-identity-wallet/templates/miw-ingress.yaml @@ -1,4 +1,4 @@ -# ******************************************************************************** +# /******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************** +# ********************************************************************************/ {{ if .Values.ingress.enabled -}} {{- $fullName := include "managed-identity-wallet.fullname" . -}} diff --git a/charts/managed-identity-wallet/templates/miw-secret.yaml b/charts/managed-identity-wallet/templates/miw-secret.yaml index ff3af397..832ecf87 100644 --- a/charts/managed-identity-wallet/templates/miw-secret.yaml +++ b/charts/managed-identity-wallet/templates/miw-secret.yaml @@ -1,4 +1,4 @@ -# ******************************************************************************** +# /******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************** +# ********************************************************************************/ {{ if .Values.secrets -}} apiVersion: v1 diff --git a/charts/managed-identity-wallet/templates/miw-service.yaml b/charts/managed-identity-wallet/templates/miw-service.yaml index 4dd6103d..8c067a45 100644 --- a/charts/managed-identity-wallet/templates/miw-service.yaml +++ b/charts/managed-identity-wallet/templates/miw-service.yaml @@ -1,4 +1,4 @@ -# ******************************************************************************** +# /******************************************************************************** # * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional @@ -15,7 +15,7 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************** +# ********************************************************************************/ apiVersion: v1 kind: Service diff --git a/charts/managed-identity-wallet/templates/networkpolicy.yaml b/charts/managed-identity-wallet/templates/networkpolicy.yaml index 7fa2a38d..425016e6 100644 --- a/charts/managed-identity-wallet/templates/networkpolicy.yaml +++ b/charts/managed-identity-wallet/templates/networkpolicy.yaml @@ -1,5 +1,5 @@ -# ******************************************************************************** -# * Copyright (c) 2024 Contributors to the Eclipse Foundation +# /******************************************************************************** +# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional # * information regarding copyright ownership. @@ -15,7 +15,8 @@ # * under the License. # * # * SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************** +# ********************************************************************************/ + {{- if .Values.networkPolicy.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy diff --git a/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml b/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml index 35117541..53fd2be4 100644 --- a/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml +++ b/charts/managed-identity-wallet/templates/pgAdmin-server-definitions.yaml @@ -21,7 +21,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: pgadmin4-server-definitions + name: {{ .Release.Name }}-pgadmin4-server-definitions labels: {{- include "pgadmin.labels" . | nindent 4 }} data: diff --git a/charts/managed-identity-wallet/templates/psql-pv.yaml b/charts/managed-identity-wallet/templates/psql-pv.yaml index 828a4e87..7db26605 100644 --- a/charts/managed-identity-wallet/templates/psql-pv.yaml +++ b/charts/managed-identity-wallet/templates/psql-pv.yaml @@ -1,3 +1,22 @@ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### + apiVersion: v1 kind: PersistentVolumeClaim metadata: diff --git a/charts/managed-identity-wallet/templates/serviceaccount.yaml b/charts/managed-identity-wallet/templates/serviceaccount.yaml index 83a53039..f2824f59 100644 --- a/charts/managed-identity-wallet/templates/serviceaccount.yaml +++ b/charts/managed-identity-wallet/templates/serviceaccount.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### {{ if .Values.serviceAccount.create -}} apiVersion: v1 diff --git a/charts/managed-identity-wallet/templates/vcrs-ingress.yaml b/charts/managed-identity-wallet/templates/vcrs-ingress.yaml index 0f36912b..22e49025 100644 --- a/charts/managed-identity-wallet/templates/vcrs-ingress.yaml +++ b/charts/managed-identity-wallet/templates/vcrs-ingress.yaml @@ -17,12 +17,13 @@ # SPDX-License-Identifier: Apache-2.0 ############################################################### -{{- if .Values.vcrs.ingress.enabled -}} + +{{ if .Values.vcrs.ingress.enabled -}} {{- $fullName := include "verifiable-credential-revocation-service.fullname" . -}} -{{- $svcPort := .Values.vcrs.ingress.service.port -}} -{{- if and .Values.ingress.vcrs.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.vcrs.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.vcrs.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.vcrs.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.vcrs.className}} + {{- $_ := set .Values.vcrs.ingress.annotations "kubernetes.io/ingress.class" .Values.vcrs.ingress.className }} {{- end }} {{- end }} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} @@ -37,27 +38,27 @@ metadata: name: {{ $fullName }} labels: {{- include "verifiable-credential-revocation-service.labels" . | nindent 4 }} - {{- with .Values.ingress.vcrs.annotations }} + {{- with .Values.vcrs.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - {{- if and .Values.ingress.vcrs.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.vcrs.className }} + {{- if and .Values.vcrs.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.vcrs.ingress.className }} {{- end }} - {{- if .Values.ingress.vcrs.tls }} + {{- if .Values.vcrs.ingress.tls }} tls: - {{- range .Values.ingress.vcrs.tls }} + {{- range .Values.vcrs.ingress.tls }} - hosts: {{- range .hosts }} - - {{ . | quote }} + - {{ tpl . $ | quote }} {{- end }} - secretName: {{ .secretName }} + secretName: "{{ $fullName }}-{{ .secretName }}" {{- end }} {{- end }} rules: - {{- range .Values.ingress.vcrs.hosts }} - - host: {{ .host | quote }} + {{- range .Values.vcrs.ingress.hosts }} + - host: {{ tpl .host $ | quote }} http: paths: {{- range .paths }} @@ -77,4 +78,4 @@ spec: {{- end }} {{- end }} {{- end }} -{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 8f627ff0..c2530500 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -51,22 +51,20 @@ service: port: 8080 # -- Ingress Configuration ingress: - # -- Enable ingress controller resource - enabled: false - # -- Ingress annotations - annotations: {} - # -- Ingress accepted hostnames - hosts: [] - # - host: chart-example.local - # paths: - # - path: / - # pathType: Prefix - # -- Ingress TLS configuration - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - className: nginx + enabled: true + className: "nginx" + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" + hosts: + - host: miw.example.org + paths: + - path: / + pathType: Prefix + tls: + - secretName: chart-example-tls + hosts: + - miw.example.org # -- Pod security configurations podSecurityContext: {} # -- Pod security parameters @@ -451,22 +449,21 @@ vcrs: # -- Number of seconds after which the probe times out. timeoutSeconds: 15 # -- ingress configuration - ingressName: "verifiable-credential-revocation-service-ingress" ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" + enabled: true + className: "nginx" + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" hosts: - - host: chart-example.local + - host: vcrs.example.org paths: - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local + pathType: Prefix + tls: + - secretName: chart-example-tls + hosts: + - vcrs.example.org service: # -- Kubernetes Service type type: ClusterIP From 52281f2ceb536d2518b0996e8f1f7b4d732dae25 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Thu, 29 Aug 2024 10:35:24 +0530 Subject: [PATCH 45/60] docs: code changes --- charts/deployChart.yaml | 0 charts/managed-identity-wallet/Chart.yaml | 10 ++- .../templates/NOTES.txt | 1 + .../templates/vcrs-ingress.yaml | 1 - .../tests/custom-values/deployment_test.yaml | 4 +- .../tests/custom-values/ingress_test.yaml | 4 +- .../tests/custom-values/secret_test.yaml | 6 +- .../tests/default/deployment_test.yaml | 10 +-- .../tests/default/ingress_test.yaml | 2 +- .../tests/default/service_test.yaml | 4 +- charts/managed-identity-wallet/values.yaml | 66 +++++++------------ 11 files changed, 41 insertions(+), 67 deletions(-) create mode 100644 charts/deployChart.yaml diff --git a/charts/deployChart.yaml b/charts/deployChart.yaml new file mode 100644 index 00000000..e69de29b diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 809b7a64..b21055dc 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -22,20 +22,28 @@ name: managed-identity-wallet description: | Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. + type: application version: 1.0.0-develop.4 appVersion: 1.0.0-develop.4 +home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: - Managed Identity Wallet - eclipse-tractusx + sources: - https://github.com/eclipse-tractusx/managed-identity-wallet + maintainers: - name: Dominik Pinsel email: dominik.pinsel@mercedes-benz.com url: https://github.com/DominikPinsel + - name: Rohit Solanki + email: rohit.solanki@smartsensesolutions.com + url: https://github.com/rohit-smartsensesolutions + dependencies: - name: keycloak version: 15.1.6 @@ -51,7 +59,7 @@ dependencies: repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: pgadmin4 - repository: file://charts/pgadmin4 # https://helm.runix.net + repository: file://charts/pgadmin4 # https://helm.runix.net # License: https://github.com/rowanruseler/helm-charts/blob/main/LICENSE version: 1.19.0 condition: pgadmin4.enabled diff --git a/charts/managed-identity-wallet/templates/NOTES.txt b/charts/managed-identity-wallet/templates/NOTES.txt index 2c3e36f9..320fef15 100644 --- a/charts/managed-identity-wallet/templates/NOTES.txt +++ b/charts/managed-identity-wallet/templates/NOTES.txt @@ -3,6 +3,7 @@ {{- range $host := .Values.ingress.hosts }} {{- range .paths }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + http{{ if $.Values.vcrs.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} diff --git a/charts/managed-identity-wallet/templates/vcrs-ingress.yaml b/charts/managed-identity-wallet/templates/vcrs-ingress.yaml index 22e49025..b97a5eac 100644 --- a/charts/managed-identity-wallet/templates/vcrs-ingress.yaml +++ b/charts/managed-identity-wallet/templates/vcrs-ingress.yaml @@ -17,7 +17,6 @@ # SPDX-License-Identifier: Apache-2.0 ############################################################### - {{ if .Values.vcrs.ingress.enabled -}} {{- $fullName := include "verifiable-credential-revocation-service.fullname" . -}} {{- $svcPort := .Values.service.port -}} diff --git a/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml b/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml index e7436abc..1da5d8bc 100644 --- a/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml @@ -22,7 +22,7 @@ chart: version: 9.9.9+test appVersion: 9.9.9 templates: - - templates/deployment.yaml + - templates/miw-deployment.yaml tests: - it: should have latest image tag values: @@ -31,7 +31,6 @@ tests: - matchRegex: path: spec.template.spec.containers[0].image pattern: .:latest - - it: should have environment variables set (envs and secrets set) values: - values.yml @@ -93,4 +92,3 @@ tests: secretKeyRef: key: encryption-key name: RELEASE-NAME-managed-identity-wallet - diff --git a/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml b/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml index ba240c6f..4513df95 100644 --- a/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml @@ -22,7 +22,7 @@ chart: version: 9.9.9+test appVersion: 9.9.9 templates: - - templates/ingress.yaml + - templates/miw-ingress.yaml values: - values.yml tests: @@ -42,7 +42,6 @@ tests: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/version: "9.9.9" app.kubernetes.io/managed-by: Helm - - it: must have rules set asserts: - isNotEmpty: @@ -57,7 +56,6 @@ tests: count: 1 - isNotEmpty: path: spec.rules[0].http.paths[0].path - - it: must have tls set asserts: - isNotEmpty: diff --git a/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml b/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml index 4ca3a80b..671f82f1 100644 --- a/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml @@ -22,7 +22,7 @@ chart: version: 9.9.9+test appVersion: 9.9.9 templates: - - templates/secret.yaml + - templates/miw-secret.yaml values: - values.yml tests: @@ -38,20 +38,16 @@ tests: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/version: "9.9.9" app.kubernetes.io/managed-by: Helm - - it: must have type set to Opaque asserts: - equal: path: type value: Opaque - - it: must have data set asserts: - isNotEmpty: path: data - - it: must have values in data asserts: - exists: path: data.encryption-key - diff --git a/charts/managed-identity-wallet/tests/default/deployment_test.yaml b/charts/managed-identity-wallet/tests/default/deployment_test.yaml index cdc11c2e..f1dc0f09 100644 --- a/charts/managed-identity-wallet/tests/default/deployment_test.yaml +++ b/charts/managed-identity-wallet/tests/default/deployment_test.yaml @@ -22,7 +22,7 @@ chart: version: 9.9.9+test appVersion: 9.9.9 templates: - - templates/deployment.yaml + - templates/miw-deployment.yaml tests: - it: should have correct metadata asserts: @@ -40,7 +40,6 @@ tests: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/version: "9.9.9" app.kubernetes.io/managed-by: Helm - - it: should have important values set asserts: - equal: @@ -57,14 +56,12 @@ tests: name: http containerPort: 8080 protocol: TCP - - it: should have probes set asserts: - isNotEmpty: path: spec.template.spec.containers[0].livenessProbe - isNotEmpty: path: spec.template.spec.containers[0].readinessProbe - - it: should have resource limits set asserts: - isNotEmpty: @@ -81,7 +78,6 @@ tests: path: spec.template.spec.containers[0].resources.requests.cpu - isNotEmpty: path: spec.template.spec.containers[0].resources.requests.memory - - it: should have a security context asserts: - isSubset: @@ -92,7 +88,6 @@ tests: runAsGroup: 11111 runAsNonRoot: true runAsUser: 11111 - - it: should have environment variables set asserts: - isNotEmpty: @@ -147,14 +142,12 @@ tests: value: "8080" - name: VC_EXPIRY_DATE value: 31-12-2024 - - it: should have empty values asserts: - notExists: path: spec.template.spec.affinity - notExists: path: spec.template.spec.tolerations - - it: should have nodeSelector value set asserts: - exists: @@ -163,7 +156,6 @@ tests: path: spec.template.spec.nodeSelector content: "kubernetes.io/os": linux - - it: should not have "imagePullSecrets" set asserts: - notExists: diff --git a/charts/managed-identity-wallet/tests/default/ingress_test.yaml b/charts/managed-identity-wallet/tests/default/ingress_test.yaml index 8217e084..ceb5fac0 100644 --- a/charts/managed-identity-wallet/tests/default/ingress_test.yaml +++ b/charts/managed-identity-wallet/tests/default/ingress_test.yaml @@ -22,7 +22,7 @@ chart: version: 9.9.9+test appVersion: 9.9.9 templates: - - templates/ingress.yaml + - templates/miw-ingress.yaml tests: - it: should not be available asserts: diff --git a/charts/managed-identity-wallet/tests/default/service_test.yaml b/charts/managed-identity-wallet/tests/default/service_test.yaml index a4287974..ba21c898 100644 --- a/charts/managed-identity-wallet/tests/default/service_test.yaml +++ b/charts/managed-identity-wallet/tests/default/service_test.yaml @@ -22,7 +22,7 @@ chart: version: 9.9.9+test appVersion: 9.9.9 templates: - - templates/service.yaml + - templates/miw-service.yaml tests: - it: should have correct metadata asserts: @@ -40,13 +40,11 @@ tests: app.kubernetes.io/instance: RELEASE-NAME app.kubernetes.io/version: "9.9.9" app.kubernetes.io/managed-by: Helm - - it: should have type set to ClusterIP asserts: - equal: path: spec.type value: ClusterIP - - it: should have ports set asserts: - contains: diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index c2530500..806f704c 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -32,7 +32,6 @@ image: pullPolicy: Always # -- Image tag (empty one will use "appVersion" value from chart definition) tag: "" -imagePullSecrets: [] # -- Parameters for the application (will be stored as secrets - so, for passwords, ...) secrets: {} # -- envs Parameters for the application (will be provided as environment variables) @@ -50,21 +49,23 @@ service: # -- Kubernetes Service port port: 8080 # -- Ingress Configuration + ingress: - enabled: true - className: "nginx" - annotations: - kubernetes.io/ingress.class: "nginx" - kubernetes.io/tls-acme: "true" - hosts: - - host: miw.example.org - paths: - - path: / - pathType: Prefix - tls: - - secretName: chart-example-tls - hosts: - - miw.example.org + # -- Enable ingress controller resource + enabled: false + # -- Ingress annotations + annotations: {} + # -- Ingress accepted hostnames + hosts: [] + # - host: chart-example.local + # paths: + # - path: / + # pathType: Prefix + # -- Ingress TLS configuration + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local # -- Pod security configurations podSecurityContext: {} # -- Pod security parameters @@ -142,13 +143,13 @@ miw: # -- Database port port: 5432 # -- Database host - host: "managed-identity-wallet-postgresql" + host: "{{ .Release.Name }}-postgresql" # -- Database user user: "miw" # -- Database name name: "miw_app" # -- Existing secret name for the database password - secret: "managed-identity-wallet-postgresql" + secret: "{{ .Release.Name }}-postgresql" # -- Existing secret key for the database password secretPasswordKey: "password" # -- Password encryption configuratons @@ -192,45 +193,32 @@ miw: successThreshold: 1 # -- Number of seconds after which the probe times out. timeoutSeconds: 5 - # For more information on how to configure the Keycloak chart see https://github.com/bitnami/charts/tree/main/bitnami/keycloak. # ----------------------------------------------- KEYCLOAK ----------------------------------------------- # +# For more information on how to configure the Keycloak chart see https://github.com/bitnami/charts/tree/main/bitnami/keycloak. keycloak: # -- Enable to deploy Keycloak enabled: true # -- Extra environment variables extraEnvVars: [] - # - name: KEYCLOAK_HOSTNAME - # value: "keycloak" + # - name: KEYCLOAK_HOSTNAME + # value: "{{ .Release.Name }}-keycloak" postgresql: # -- Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. nameOverride: "keycloak-postgresql" # -- Enable to deploy PostgreSQL enabled: true auth: - # -- Postgresql admin user password + # -- Keycloak PostgreSQL user username: "miw_keycloak" # -- KeycloakPostgresql password to set (if empty one is generated) - password: "adminpass" + password: "" # -- Database name database: "miw_keycloak" - volumePermissions: - enabled: true ingress: - # -- Enable ingress controller resource enabled: false - # -- Ingress annotations annotations: {} - # -- Ingress accepted hostnames hosts: [] - # - host: chart-example.local - # paths: - # - path: / - # pathType: Prefix - # -- Ingress TLS configuration tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local auth: # -- Keycloak admin user adminUser: "admin" @@ -248,10 +236,6 @@ keycloak: postgresql: # -- Enable to deploy Postgresql enabled: true - image: - tag: "16-debian-12" - # -- Debug logs - debug: true auth: # -- Enable postgresql admin user enablePostgresUser: true @@ -375,7 +359,7 @@ vcrs: # -- The application profile APPLICATION_PROFILE: local # -- The Database Host - DATABASE_HOST: managed-identity-wallet-postgresql + DATABASE_HOST: "{{ .Release.Name }}-postgresql" # -- The Database Port DATABASE_PORT: 5432 # -- The Database Name @@ -450,7 +434,7 @@ vcrs: timeoutSeconds: 15 # -- ingress configuration ingress: - enabled: true + enabled: false className: "nginx" annotations: kubernetes.io/ingress.class: "nginx" From c0c202fb27120fda1cab293cceea58551322cae9 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Thu, 5 Sep 2024 12:27:06 +0530 Subject: [PATCH 46/60] docs: copyright header and README file modification --- charts/deployChart.yaml | 0 charts/managed-identity-wallet/Chart.yaml | 36 +++++++++---------- charts/managed-identity-wallet/README.md | 8 ++--- .../templates/miw-deployment.yaml | 36 +++++++++---------- .../tests/custom-values/deployment_test.yaml | 36 +++++++++---------- .../tests/custom-values/ingress_test.yaml | 36 +++++++++---------- .../tests/custom-values/secret_test.yaml | 36 +++++++++---------- .../tests/default/deployment_test.yaml | 36 +++++++++---------- .../tests/default/ingress_test.yaml | 36 +++++++++---------- .../tests/default/service_test.yaml | 36 +++++++++---------- charts/managed-identity-wallet/values.yaml | 3 +- 11 files changed, 150 insertions(+), 149 deletions(-) delete mode 100644 charts/deployChart.yaml diff --git a/charts/deployChart.yaml b/charts/deployChart.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index b21055dc..9e31ac68 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### apiVersion: v2 name: managed-identity-wallet diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 3228e7d3..fb5ba79c 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -43,7 +43,7 @@ And at the same it shall support an uninterrupted tracking and tracing and docum helm install [RELEASE_NAME] charts/managed-identity-wallet - #This will spin up the container for MIW application, VSRS application, KeyCloak and Postgresql + #This will spin up the container for Managed Identity Wallet application, Verifiable Credential Revocation Service application, Keycloak and Postgresql

(back to top)

@@ -194,9 +194,9 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | | tolerations | list | `[]` | Tolerations configuration | | vcrs.replicaCount | int | `1` | Number of replicas to run | -| vcrs.url | string | `"https://a888-203-129-213-107.ngrok-free.app"` | Application URL | -| vcrs.vcContexts | string | `"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"` | App VC context | -| vcrs.domain.url | string | `"https://977d-203-129-213-107.ngrok-free.app"` | App domain | +| vcrs.url | string | `"https://example.com"` | Application URL | +| vcrs.vcContexts | string | `"https://www.w3.org/2018/credentials/v1, https://w3id.org/vc/status-list/2021/v1"` | App VC context | +| vcrs.domain.url | string | `"https://example.com"` | App domain | | vcrs.domain.host | string | `"localhost"` | The application name | | vcrs.appName | string | `"verifiable-credential-revocation-service"` | The configmap name | | vcrs.appPort | string | `"8081"` | The application port | diff --git a/charts/managed-identity-wallet/templates/miw-deployment.yaml b/charts/managed-identity-wallet/templates/miw-deployment.yaml index 0e147aa1..bb740127 100644 --- a/charts/managed-identity-wallet/templates/miw-deployment.yaml +++ b/charts/managed-identity-wallet/templates/miw-deployment.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### apiVersion: apps/v1 kind: Deployment diff --git a/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml b/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml index 1da5d8bc..029f0e0a 100644 --- a/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### suite: test custom-values deployment chart: diff --git a/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml b/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml index 4513df95..1887e72b 100644 --- a/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/ingress_test.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### suite: test custom-values ingress chart: diff --git a/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml b/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml index 671f82f1..5f99bc2c 100644 --- a/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/secret_test.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### suite: test custom-values secret chart: diff --git a/charts/managed-identity-wallet/tests/default/deployment_test.yaml b/charts/managed-identity-wallet/tests/default/deployment_test.yaml index f1dc0f09..1e2d3d0b 100644 --- a/charts/managed-identity-wallet/tests/default/deployment_test.yaml +++ b/charts/managed-identity-wallet/tests/default/deployment_test.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### suite: test default deployment chart: diff --git a/charts/managed-identity-wallet/tests/default/ingress_test.yaml b/charts/managed-identity-wallet/tests/default/ingress_test.yaml index ceb5fac0..02e2735d 100644 --- a/charts/managed-identity-wallet/tests/default/ingress_test.yaml +++ b/charts/managed-identity-wallet/tests/default/ingress_test.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### suite: test default ingress chart: diff --git a/charts/managed-identity-wallet/tests/default/service_test.yaml b/charts/managed-identity-wallet/tests/default/service_test.yaml index ba21c898..34f72998 100644 --- a/charts/managed-identity-wallet/tests/default/service_test.yaml +++ b/charts/managed-identity-wallet/tests/default/service_test.yaml @@ -1,21 +1,21 @@ -# /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation -# * -# * See the NOTICE file(s) distributed with this work for additional -# * information regarding copyright ownership. -# * -# * This program and the accompanying materials are made available under the -# * terms of the Apache License, Version 2.0 which is available at -# * https://www.apache.org/licenses/LICENSE-2.0. -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# * License for the specific language governing permissions and limitations -# * under the License. -# * -# * SPDX-License-Identifier: Apache-2.0 -# ********************************************************************************/ +############################################################### +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +############################################################### suite: test default service chart: diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 806f704c..327e51b1 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -48,8 +48,9 @@ service: type: ClusterIP # -- Kubernetes Service port port: 8080 +# -- Image pull secrets +imagePullSecrets: [] # -- Ingress Configuration - ingress: # -- Enable ingress controller resource enabled: false From 643493df5b862bbc1a86b30360706180d78aa19e Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Thu, 5 Sep 2024 15:11:54 +0530 Subject: [PATCH 47/60] fix: sonar issues --- .../identityminustrust/TokenRequestTest.java | 4 +- .../utils/TestUtils.java | 3 +- .../services/RevocationService.java | 2 +- .../VerifiableCredentialValidator.java | 2 +- .../revocation/config/MIWSettingsTest.java | 3 +- .../CustomAuthenticationConverterTest.java | 14 ------ .../RevocationApiControllerTest.java | 25 ++--------- .../dto/CredentialStatusDtoTest.java | 14 +++--- .../dto/StatusListCredentialSubjectTest.java | 4 +- .../revocation/dto/TokenResponeTest.java | 6 +-- .../services/HttpClientServiceTest.java | 7 +-- .../services/RevocationServiceTest.java | 45 +++++++++---------- .../services/StatusVerificationTest.java | 33 -------------- .../commons/ValidateTest.java | 21 ++++----- 14 files changed, 55 insertions(+), 128 deletions(-) delete mode 100644 revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java index 6c481935..ff88fada 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/identityminustrust/TokenRequestTest.java @@ -55,7 +55,7 @@ @DirtiesContext @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { ManagedIdentityWalletsApplication.class }) @ContextConfiguration(initializers = { TestContextInitializer.class }) -public class TokenRequestTest { +class TokenRequestTest { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -125,7 +125,7 @@ public void initWallets() { @Test @SneakyThrows - public void testPresentationQueryWithToken() { + void testPresentationQueryWithToken() { // when String body = "audience=%s&client_id=%s&client_secret=%s&grant_type=client_credentials&bearer_access_scope=org.eclipse.tractusx.vc.type:MembershipCredential:read"; String requestBody = String.format(body, bpn, clientId, clientSecret); diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 3eb7ed69..246eab4e 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -98,8 +98,7 @@ public static ResponseEntity createWallet(String bpn, String name, TestR HttpEntity entity = new HttpEntity<>(request, headers); - ResponseEntity exchange = testTemplate.exchange(RestURI.WALLETS, HttpMethod.POST, entity, String.class); - return exchange; + return testTemplate.exchange(RestURI.WALLETS, HttpMethod.POST, entity, String.class); } diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java index e6033e8a..4dff1776 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java @@ -363,7 +363,7 @@ public String extractBpnFromURL(String url) { if (matcher.find()) { return matcher.group(1).toUpperCase(); } else { - throw new Exception("No match found"); + throw new IllegalArgumentException("No match found"); } } diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java index 00947e53..d049f11f 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/validation/VerifiableCredentialValidator.java @@ -59,7 +59,7 @@ private boolean validateCredentialSubject( for (Map subject : credentialSubjects) { // Extract the 'id' of the credential subject if it exists Object subjectId = subject.get("id"); - if (subjectId == null || !(subjectId instanceof String)) { + if (!(subjectId instanceof String)) { addConstraintViolation(context, "credentialSubject.id must be a valid String"); return false; } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java index c04163e8..4ee8efe2 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/MIWSettingsTest.java @@ -50,6 +50,7 @@ void testMIWSettingsWithNullVCContexts() { @Test void testMIWSettingsWithEmptyVCContexts() { - assertThrows(IllegalArgumentException.class, () -> new MIWSettings(List.of())); + List list = List.of(); + assertThrows(IllegalArgumentException.class, () -> new MIWSettings(list)); } } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java index a918b0fe..b5b050c6 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/config/security/CustomAuthenticationConverterTest.java @@ -36,19 +36,6 @@ class CustomAuthenticationConverterTest { - private static final String VALID = - "eyJhbGciOiJSUzI1NiIsImFscGhhIjoiZzB1ZjNycjlycnN2cHlhcTVuamg4In0.eyJpc3MiOiJEaW5vQ2hpZXNhLmdpdGh1Yi5pbyIsInN1YiI6Im1heGluZSIsImF1ZCI6ImlkcmlzIiwiaWF0IjoxNzAyNjUwMTc2LCJleHAiOjE3MDI2NTA3NzYsInJlc291cmNlX2FjY2VzcyI6eyJyZXNvdXJjZUlkIjp7InJvbGVzIjpbImRlaV9tdWRhIl19fX0.wTv9GBX3AuRO8UIsAuu2YJU77ai-wchDyxRn-_yX9PeHt23vCmp_JAbkkdMdyLAWWOKncjgNeG-4lB9RCBsjmbdb1imujUrAocp3VZQqNg6OVaNV58kdsIpNNF9S8XlFI4hr1BANrw2rWJDkTRu1id-Fu-BVE1BF7ySCKHS_NaY3e7yXQM-jtU63z5FBpPvfMF-La3blPle93rgut7V3LlG-tNOp93TgFzGrQQXuJUsew34T0u4OlQa3TjQuMdZMTy0SVSLSpIzAqDsAkHv34W6SdY1p6FVQ14TfawRLkrI2QY-YM_dCFAEE7KqqnUrVVyw6XG1ydeFDuX8SJuQX7g"; - - private static final String MISSING_RESOURCE_ID = - "{\n" + " \"resource_access\": {\n" + " }\n" + "}"; - - private static final String MISSING_ROLES = - "{\n" - + " \"resource_access\": {\n" - + " \"resourceId\": {\n" - + " }\n" - + " }\n" - + "}"; @Test void shouldConvertSuccessfullyWithAuthorities() { @@ -92,7 +79,6 @@ void shouldConvertSuccessfullyWithoutAuthoritiesWhenRolesMissing() { @Test void shouldConvertSuccessfullyWithoutAuthoritiesWhenResourceAccessMissing() { - Map resourceId = Map.of("resourceId", Map.of()); Map resourceAccess = Map.of("resource_access", Map.of()); Jwt jwt = diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java index ff11b541..6f0379f4 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/controllers/RevocationApiControllerTest.java @@ -27,6 +27,7 @@ import org.eclipse.tractusx.managedidentitywallets.revocation.constant.RevocationApiEndpoints; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.CredentialStatusDto; import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusEntryDto; +import org.eclipse.tractusx.managedidentitywallets.revocation.dto.StatusListCredentialSubject; import org.eclipse.tractusx.managedidentitywallets.revocation.services.RevocationService; import org.eclipse.tractusx.managedidentitywallets.revocation.utils.BitSetManager; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; @@ -99,13 +100,12 @@ void whenPostCreateStatusListVC_thenReturnStatus() throws Exception { RevocationPurpose.REVOCATION.name(), validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] "https://example.com/revocations/credentials/" + BPN + "/revocation/1", - "BitstringStatusListEntry"); + StatusListCredentialSubject.TYPE_ENTRY); given(revocationService.createStatusList(statusEntryDto, "token")) .willReturn(credentialStatusDto); when(revocationService.extractBpnFromDid(DID)).thenReturn(BPN); Principal mockPrincipal = mockPrincipal(BPN); - var name = mockPrincipal.getName(); // When & Then mockMvc .perform( @@ -142,7 +142,7 @@ void whenPostRevokeCredential_thenReturnOkStatus() throws Exception { "revocation", validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] "http://example.com/credentials/" + BPN + "/revocation/1", - "BitstringStatusListEntry"); + StatusListCredentialSubject.TYPE_ENTRY); doNothing().when(revocationService).revoke(credentialStatusDto, "token"); when(revocationService.extractBpnFromURL(any())).thenReturn(BPN); @@ -201,23 +201,4 @@ private VerifiableCredential createVerifiableCredentialTestData() { VerifiableCredential credential = new VerifiableCredential(credentialData); return credential; } - - private VerifiableCredential createVerifiableCredentialTestDataInvalidDID() { - Map credentialData = new HashMap<>(); - credentialData.put("id", UUID.randomUUID().toString()); - credentialData.put("issuer", "https://issuer.example.com"); - credentialData.put("issuanceDate", Instant.now().toString()); - // Include 'type' field as a list because VerifiableCredential expects it to be non-null and a - // list - credentialData.put("type", List.of("VerifiableCredential", "StatusListCredential")); - Map subjectData = new HashMap<>(); - subjectData.put("id", "subjectId"); - subjectData.put("type", "StatusList2021Credential"); - // 'credentialSubject' can be either a List or a single Map according to the code, so I'm - // keeping it as a single Map - credentialData.put("credentialSubject", subjectData); - credentialData.put("@context", VerifiableCredential.DEFAULT_CONTEXT.toString()); - VerifiableCredential credential = new VerifiableCredential(credentialData); - return credential; - } } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java index 43235a50..83e966ff 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/CredentialStatusDtoTest.java @@ -50,7 +50,7 @@ void validCredentialStatusDto_CreatesSuccessfully() { "revocation", validIndex, // this value is within the range [0, BitSetManager.BITSET_SIZE - 1] "statusListCredential", - "BitstringStatusListEntry"); + StatusListCredentialSubject.TYPE_ENTRY); // Assert assertNotNull(dto); @@ -58,7 +58,7 @@ void validCredentialStatusDto_CreatesSuccessfully() { assertEquals("revocation", dto.statusPurpose()); assertEquals(validIndex, dto.statusListIndex()); assertEquals("statusListCredential", dto.statusListCredential()); - assertEquals("BitstringStatusListEntry", dto.type()); + assertEquals(StatusListCredentialSubject.TYPE_ENTRY, dto.type()); } @ParameterizedTest @@ -89,7 +89,7 @@ void anyParameterIsBlank_ThrowsValidationException() { "revocation", "0", "statusListCredential", - "BitstringStatusListEntry")) + StatusListCredentialSubject.TYPE_ENTRY)) .isEmpty()); assertFalse( @@ -100,7 +100,7 @@ void anyParameterIsBlank_ThrowsValidationException() { "revocation", "0", "", // statusListCredential is blank - "BitstringStatusListEntry")) + StatusListCredentialSubject.TYPE_ENTRY)) .isEmpty()); } @@ -113,7 +113,7 @@ void invalidStatusPurpose_ThrowsIllegalArgumentException() { IllegalArgumentException.class, () -> { new CredentialStatusDto( - "id", invalidPurpose, "0", "statusListCredential", "BitstringStatusListEntry"); + "id", invalidPurpose, "0", "statusListCredential", StatusListCredentialSubject.TYPE_ENTRY); }); } @@ -137,14 +137,14 @@ void validStatusPurpose_DoesNotThrowException() { assertDoesNotThrow( () -> { new CredentialStatusDto( - "id", validPurpose, "0", "statusListCredential", "BitstringStatusListEntry"); + "id", validPurpose, "0", "statusListCredential", StatusListCredentialSubject.TYPE_ENTRY); }); } @Test @DisplayName("type is valid") void validType_DoesNotThrowException() { - String validType = "BitstringStatusListEntry"; + String validType = StatusListCredentialSubject.TYPE_ENTRY; assertDoesNotThrow( () -> { diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java index 246c8473..f720cf9b 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/StatusListCredentialSubjectTest.java @@ -55,8 +55,8 @@ void builderCreatesObjectWithCorrectValues() { @Test void defaultConstantsAreCorrect() { // Assert - assertEquals("BitstringStatusListEntry", StatusListCredentialSubject.TYPE_ENTRY); - assertEquals("BitstringStatusList", StatusListCredentialSubject.TYPE_LIST); + assertEquals("StatusList2021Entry", StatusListCredentialSubject.TYPE_ENTRY); + assertEquals("StatusList2021Credential", StatusListCredentialSubject.TYPE_LIST); assertEquals("id", StatusListCredentialSubject.SUBJECT_ID); assertEquals("type", StatusListCredentialSubject.SUBJECT_TYPE); assertEquals("statusPurpose", StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE); diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java index 6b040616..4e686960 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/dto/TokenResponeTest.java @@ -26,7 +26,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -public class TokenResponeTest { +class TokenResponseTest { @Test void getAndSetAccessToken() { // Arrange @@ -38,7 +38,7 @@ void getAndSetAccessToken() { // Assert String actualToken = tokenResponse.getAccessToken(); - assertEquals(expectedToken, actualToken, "someAccessToken123"); + assertEquals(expectedToken, "someAccessToken123", actualToken); } @Test @@ -65,6 +65,6 @@ void setAccessTokenWithEmptyString() { // Assert String actualToken = tokenResponse.getAccessToken(); - assertEquals(expectedToken, actualToken, ""); + assertEquals(expectedToken, "", actualToken); } } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java index 6f530935..31d929f9 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java @@ -26,7 +26,6 @@ import org.eclipse.tractusx.managedidentitywallets.revocation.dto.TokenResponse; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -91,10 +90,6 @@ public static void beforeAll() { ReflectionTestUtils.setField(httpClientService, "miwUrl", wm1.baseUrl()); } - @BeforeEach - void setUp() { - } - @Test void testGetBearerToken_Success() { String expectedToken = "mockToken"; @@ -131,7 +126,7 @@ void testSignStatusListVC_Success() { tokenResponse.setAccessToken("123456"); wm1.stubFor(post("/token").willReturn(jsonResponse(tokenResponse, 200))); wm1.stubFor( - post("/api/credentials?isRevocable=false") + post("/api/credentials?revocable=false") .willReturn(jsonResponse(statusListCredential.getCredential(), 200))); VerifiableCredential signedCredential = assertDoesNotThrow( diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java index 728ec0d2..6c97acc7 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationServiceTest.java @@ -21,7 +21,6 @@ package org.eclipse.tractusx.managedidentitywallets.revocation.services; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.SneakyThrows; import org.eclipse.tractusx.managedidentitywallets.commons.constant.CredentialStatus; import org.eclipse.tractusx.managedidentitywallets.commons.constant.StringPool; @@ -40,7 +39,6 @@ import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredential; import org.eclipse.tractusx.ssi.lib.model.verifiable.credential.VerifiableCredentialSubject; import org.eclipse.tractusx.ssi.lib.proof.LinkedDataProofValidation; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -76,7 +74,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; @@ -142,7 +139,7 @@ void shouldVerifyStatusActive() { "http://this-is-my-domain/api/v1/revocations/credentials/" + TestUtil.extractBpnFromDid(issuer) + "/revocation/1"); - when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + when(credentialStatusDto.type()).thenReturn("StatusList2021Entry"); try (MockedStatic utils = Mockito.mockStatic(LinkedDataProofValidation.class)) { @@ -152,7 +149,7 @@ void shouldVerifyStatusActive() { }).thenReturn(mock); Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); Map status = revocationService.verifyStatus(credentialStatusDto); - Assertions.assertTrue(status.get(StringPool.STATUS).equals(CredentialStatus.ACTIVE.getName())); + assertEquals(status.get(StringPool.STATUS), CredentialStatus.ACTIVE.getName()); } } @@ -188,7 +185,7 @@ void shouldVerifyStatusRevoke() { "http://this-is-my-domain/api/v1/revocations/credentials/" + TestUtil.extractBpnFromDid(issuer) + "/revocation/1"); - when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + when(credentialStatusDto.type()).thenReturn("StatusList2021Entry"); try (MockedStatic utils = Mockito.mockStatic(LinkedDataProofValidation.class)) { LinkedDataProofValidation mock = Mockito.mock(LinkedDataProofValidation.class); utils.when(() -> { @@ -197,7 +194,7 @@ void shouldVerifyStatusRevoke() { Mockito.when(mock.verify(Mockito.any(VerifiableCredential.class))).thenReturn(true); Map status = revocationService.verifyStatus(credentialStatusDto); - Assertions.assertTrue(status.get(StringPool.STATUS).equals(CredentialStatus.REVOKED.getName())); + assertEquals(status.get(StringPool.STATUS), CredentialStatus.REVOKED.getName()); } } } @@ -230,10 +227,10 @@ void shouldRevokeCredential() { "http://this-is-my-domain/api/v1/revocations/credentials/" + TestUtil.extractBpnFromDid(issuer) + "/revocation/1"); - when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + when(credentialStatusDto.type()).thenReturn("StatusList2021Entry"); assertDoesNotThrow(() -> revocationService.revoke(credentialStatusDto, "token")); Mockito.verify(statusListCredentialRepository, Mockito.times(1)) - .saveAndFlush(eq(statusListCredential)); + .saveAndFlush(statusListCredential); ArgumentCaptor captor = ArgumentCaptor.forClass(VerifiableCredential.class); Mockito.verify(httpClientService) @@ -274,7 +271,7 @@ void shouldThrowRevocationServiceException() { "http://this-is-my-domain/api/v1/revocations/credentials/" + TestUtil.extractBpnFromDid(issuer) + "/revocation/1"); - when(credentialStatusDto.type()).thenReturn("BitstringStatusListEntry"); + when(credentialStatusDto.type()).thenReturn("StatusList2021Entry"); try (MockedStatic utilities = Mockito.mockStatic(BitSetManager.class)) { utilities .when(() -> BitSetManager.revokeCredential(any(String.class), any(Integer.class))) @@ -370,7 +367,7 @@ void shouldCreateNewStatusListWhenFirstFull() { class GetStatusListCredential { @Test - void shouldGetList() throws JsonProcessingException { + void shouldGetList() { final var issuer = DID; var fragment = UUID.randomUUID().toString(); var encodedList = mockEmptyEncodedList(); @@ -403,39 +400,39 @@ void shouldReturnNull() { class CheckSubStringExtraction { @Test void shouldExtractBpnFromDid() { - assertEquals(revocationService.extractBpnFromDid(DID), BPN); + assertEquals(BPN, revocationService.extractBpnFromDid(DID)); } @Test void shouldExtractIdFromURL() { assertEquals( + "BPNL123456789000-revocation#1", revocationService.extractIdFromURL( - "http://this-is-my-domain/api/v1/revocations/credentials/BPNL123456789000/revocation/1"), - "BPNL123456789000-revocation#1"); + "http://this-is-my-domain/api/v1/revocations/credentials/BPNL123456789000/revocation/1")); } @Test void shouldExtractIdFromURLCaseSensitive() { assertEquals( + "BPNL123456789000-revocation#1", revocationService.extractIdFromURL( - "http://this-is-my-domain/api/v1/revocations/credentials/bpnl123456789000/revocation/1"), - "BPNL123456789000-revocation#1"); + "http://this-is-my-domain/api/v1/revocations/credentials/bpnl123456789000/revocation/1")); } @Test void shouldExtractBpnFromURL() { assertEquals( + BPN, revocationService.extractBpnFromURL( - "http://this-is-my-domain/api/v1/revocations/credentials/BPNL123456789000/revocation/1"), - BPN); + "http://this-is-my-domain/api/v1/revocations/credentials/BPNL123456789000/revocation/1")); } @Test void shouldExtractBpnFromURLCaseSensitive() { assertEquals( + BPN, revocationService.extractBpnFromURL( - "http://this-is-my-domain/api/v1/revocations/creDENTials/bpNl123456789000/revocation/1"), - BPN); + "http://this-is-my-domain/api/v1/revocations/creDENTials/bpNl123456789000/revocation/1")); } } @@ -452,7 +449,7 @@ void validCredentialStatusDto() { CredentialStatusDto dto = new CredentialStatusDto( - id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + id, "revocation", statusIndex, statusListCredential, "StatusList2021Entry"); assertDoesNotThrow(() -> revocationService.validateCredentialStatus(dto)); } @@ -468,7 +465,7 @@ void invalidStatusPurpose_ThrowsIllegalArgumentException() { CredentialStatusDto dto = new CredentialStatusDto( - id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + id, "revocation", statusIndex, statusListCredential, "StatusList2021Entry"); assertThrows( IllegalArgumentException.class, () -> revocationService.validateCredentialStatus(dto)); } @@ -483,7 +480,7 @@ void invalidId_ThrowsIllegalArgumentException() { CredentialStatusDto dto = new CredentialStatusDto( - id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + id, "revocation", statusIndex, statusListCredential, "StatusList2021Entry"); assertThrows( IllegalArgumentException.class, () -> revocationService.validateCredentialStatus(dto)); } @@ -498,7 +495,7 @@ void invalidStatusIndex_ThrowsIllegalArgumentException() { CredentialStatusDto dto = new CredentialStatusDto( - id, "revocation", statusIndex, statusListCredential, "BitstringStatusListEntry"); + id, "revocation", statusIndex, statusListCredential, "StatusList2021Entry"); assertThrows( IllegalArgumentException.class, () -> revocationService.validateCredentialStatus(dto)); } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java deleted file mode 100644 index 80a83706..00000000 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/StatusVerificationTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * ******************************************************************************* - * Copyright (c) 2021,2024 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ****************************************************************************** - */ - -package org.eclipse.tractusx.managedidentitywallets.revocation.services; - -import org.junit.jupiter.api.Test; - -public class StatusVerificationTest { - - - @Test - void testVerification() { - - } -} diff --git a/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java index 4a65cb34..f8e70467 100644 --- a/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java +++ b/wallet-commons/src/test/java/org/eclipse/tractusx/managedidentitywallets/commons/ValidateTest.java @@ -30,25 +30,26 @@ class ValidateTest { @Test void validateTest() { - Assertions.assertThrows(RuntimeException.class, () -> Validate.isFalse(false).launch(new RuntimeException())); + RuntimeException runtimeException = new RuntimeException(); + Assertions.assertThrows(RuntimeException.class, () -> Validate.isFalse(false).launch(runtimeException)); - Assertions.assertThrows(RuntimeException.class, () -> Validate.isTrue(true).launch(new RuntimeException())); + Assertions.assertThrows(RuntimeException.class, () -> Validate.isTrue(true).launch(runtimeException)); - Assertions.assertThrows(RuntimeException.class, () -> Validate.isNull(null).launch(new RuntimeException())); + Assertions.assertThrows(RuntimeException.class, () -> Validate.isNull(null).launch(runtimeException)); - Assertions.assertThrows(RuntimeException.class, () -> Validate.isNotNull("Test").launch(new RuntimeException())); + Assertions.assertThrows(RuntimeException.class, () -> Validate.isNotNull("Test").launch(runtimeException)); - Assertions.assertThrows(RuntimeException.class, () -> Validate.value("").isNotEmpty().launch(new RuntimeException())); + Assertions.assertThrows(RuntimeException.class, () -> Validate.value("").isNotEmpty().launch(runtimeException)); - Assertions.assertDoesNotThrow(() -> Validate.isFalse(true).launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isFalse(true).launch(runtimeException)); - Assertions.assertDoesNotThrow(() -> Validate.isTrue(false).launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isTrue(false).launch(runtimeException)); - Assertions.assertDoesNotThrow(() -> Validate.isNull("").launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isNull("").launch(runtimeException)); - Assertions.assertDoesNotThrow(() -> Validate.isNotNull(null).launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.isNotNull(null).launch(runtimeException)); - Assertions.assertDoesNotThrow(() -> Validate.value("Test").isNotEmpty().launch(new RuntimeException())); + Assertions.assertDoesNotThrow(() -> Validate.value("Test").isNotEmpty().launch(runtimeException)); } } From 65dd8124787e7897260e720d42b17e61cdb8955c Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Fri, 6 Sep 2024 11:00:04 +0530 Subject: [PATCH 48/60] fix: status list VS as JSON-LD --- .../apidocs/IssuersCredentialControllerApiDocs.java | 1 - .../revocation/services/HttpClientService.java | 1 + .../revocation/services/RevocationService.java | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java index 5ad56101..771ccda3 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/IssuersCredentialControllerApiDocs.java @@ -222,7 +222,6 @@ public class IssuersCredentialControllerApiDocs { @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) - @Tag(name = API_TAG_VERIFIABLE_CREDENTIAL_VALIDATION) @ApiResponses(value = { @ApiResponse(responseCode = "401", description = "The request could not be completed due to a failed authorization.", content = { @Content(examples = {}) }), diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java index 333c8762..dd8f61b0 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientService.java @@ -74,6 +74,7 @@ public VerifiableCredential signStatusListVC(VerifiableCredential vc, String tok UriComponentsBuilder.fromHttpUrl(miwUrl) .path("/api/credentials") .queryParam(StringPool.REVOCABLE, "false") + .queryParam(StringPool.AS_JWT, "false") .build() .toUriString(); diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java index 4dff1776..a99adabe 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/RevocationService.java @@ -182,7 +182,7 @@ public void revoke(CredentialStatusDto dto, String token) throws RevocationServi newSubject = StatusListCredentialSubject.builder() .id((String) subjectCredential.get(StatusListCredentialSubject.SUBJECT_ID)) - .type(StatusListCredentialSubject.TYPE_LIST) + .type(StatusListCredentialSubject.TYPE_CREDENTIAL) .statusPurpose( (String) subjectCredential.get(StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE)) .encodedList(newEncodedList) From df62fcc2e5c841a4cfd893d99fad22d90d34e792 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Fri, 6 Sep 2024 11:27:05 +0530 Subject: [PATCH 49/60] fix: tests --- .../managedidentitywallets/utils/TestUtils.java | 10 +++++----- .../revocation/services/HttpClientServiceTest.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 246eab4e..7e9e6289 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -280,7 +280,7 @@ public static Map getCredentialAsMap(String holderBpn, String ho public static VerifiableCredentialStatusList2021Entry getStatusListEntry(int index) { return new VerifiableCredentialStatusList2021Entry(Map.of( "id", "https://example.com/credentials/bpn123456789000/revocation/3#" + index, - "type", "BitstringStatusListEntry", + "type", "StatusList2021Entry", "statusPurpose", "revocation", "statusListIndex", String.valueOf(index), "statusListCredential", "https://example.com/credentials/bpn123456789000/revocation/3" @@ -291,7 +291,7 @@ public static VerifiableCredentialStatusList2021Entry getStatusListEntry() { int index = RandomUtils.nextInt(1, 100); return new VerifiableCredentialStatusList2021Entry(Map.of( "id", "https://example.com/credentials/bpn123456789000/revocation/3#" + index, - "type", "BitstringStatusListEntry", + "type", "StatusList2021Entry", "statusPurpose", "revocation", "statusListIndex", String.valueOf(index), "statusListCredential", "https://example.com/credentials/bpn123456789000/revocation/3" @@ -318,7 +318,7 @@ public static void mockGetStatusListVC(RevocationClient revocationClient, Object String vcString = """ { "type": [ - "VerifiableCredential" + "VerifiableCredential","StatusList2021Credential" ], "@context": [ "https://www.w3.org/2018/credentials/v1", @@ -338,7 +338,7 @@ public static void mockGetStatusListVC(RevocationClient revocationClient, Object }, "credentialSubject": { "id": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", - "type": "BitstringStatusList", + "type": "StatusList2021", "statusPurpose": "revocation", "encodedList": "##encodedList" } @@ -377,7 +377,7 @@ public static void mockGetStatusListVC(RevocationClient revocationClient, Object }, "credentialSubject": { "id": "did:key:z6MkhGTzcvb8BXh5aeoaFvb3XJ3MBmfLRamdYdXyV1pxJBce", - "type": "StatusList2021Credential", + "type": "StatusList2021", "statusPurpose": "revocation", "encodedList": "H4sIAAAAAAAA/+3BMQEAAAjAoEqzfzk/SwjUmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDXFiqoX4AAAAIA" } diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java index 31d929f9..01439c23 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/services/HttpClientServiceTest.java @@ -126,7 +126,7 @@ void testSignStatusListVC_Success() { tokenResponse.setAccessToken("123456"); wm1.stubFor(post("/token").willReturn(jsonResponse(tokenResponse, 200))); wm1.stubFor( - post("/api/credentials?revocable=false") + post("/api/credentials?revocable=false&asJwt=false") .willReturn(jsonResponse(statusListCredential.getCredential(), 200))); VerifiableCredential signedCredential = assertDoesNotThrow( From 546908b5e13ce4a0695fa5ade42a14a1abb88a2b Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Mon, 9 Sep 2024 12:58:21 +0530 Subject: [PATCH 50/60] fix: status list changed to 2021 from bitstring --- miw/README.md | 62 +++++++++---------- .../config/RevocationSettings.java | 2 +- .../service/HoldersCredentialService.java | 4 +- .../service/IssuersCredentialService.java | 4 +- miw/src/main/resources/application.yaml | 2 +- .../utils/TestUtils.java | 2 +- .../src/main/resources/application.yaml | 2 +- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/miw/README.md b/miw/README.md index 0ea651ac..d5a56ec1 100644 --- a/miw/README.md +++ b/miw/README.md @@ -287,37 +287,37 @@ This process ensures that any issues with the database schema are resolved by re # Environment Variables -| name | description | default value | -|-----------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| APPLICATION_PORT | port number of application | 8080 | -| APPLICATION_ENVIRONMENT | Environment of the application ie. local, dev, int and prod | local | -| DB_HOST | Database host | localhost | -| DB_PORT | Port of database | 5432 | -| DB_NAME | Database name | miw | -| USE_SSL | Whether SSL is enabled in database server | false | -| DB_USER_NAME | Database username | | -| DB_PASSWORD | Database password | | -| DB_POOL_SIZE | Max number of database connection acquired by application | 10 | -| KEYCLOAK_MIW_PUBLIC_CLIENT | Only needed if we want enable login with keyalock in swagger | miw_public | -| MANAGEMENT_PORT | Spring actuator port | 8090 | -| MIW_HOST_NAME | Application host name, this will be used in creation of did ie. did:web:MIW_HOST_NAME:BPN | localhost | -| ENCRYPTION_KEY | encryption key used to encrypt and decrypt private and public key of wallet | | -| AUTHORITY_WALLET_BPN | base wallet BPN number | BPNL000000000000 | -| AUTHORITY_WALLET_NAME | Base wallet name | Catena-X | -| AUTHORITY_WALLET_DID | Base wallet web did | web:did:host:BPNL000000000000 | -| VC_SCHEMA_LINK | Comma separated list of VC schema URL | https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json | -| VC_EXPIRY_DATE | Expiry date of VC (dd-MM-yyyy ie. 01-01-2025 expiry date will be 2024-12-31T18:30:00Z in VC) | 01-01-2025 | -| KEYCLOAK_REALM | Realm name of keycloak | miw_test | -| KEYCLOAK_CLIENT_ID | Keycloak private client id | | -| AUTH_SERVER_URL | Keycloak server url | | -| SUPPORTED_FRAMEWORK_VC_TYPES | Supported framework VC, provide values ie type1=value1,type2=value2 | cx-behavior-twin=Behavior Twin,cx-pcf=PCF,cx-quality=Quality,cx-resiliency=Resiliency,cx-sustainability=Sustainability,cx-traceability=ID_3.0_Trace | -| ENFORCE_HTTPS_IN_DID_RESOLUTION | Enforce https during web did resolution | true | -| CONTRACT_TEMPLATES_URL | Contract templates URL used in summary VC | https://public.catena-x.org/contracts/ | -| APP_LOG_LEVEL | Log level of application | INFO | -| AUTHORITY_SIGNING_SERVICE_TYPE | Base wallet signing type, Currency only LOCAL is supported | Local | -| LOCAL_SIGNING_KEY_STORAGE_TYPE | Key storage type, currently only DB is supported | DB | -| BITSTRING_STATUS_LIST_CONTEXT_URL | Context URI for bitstring status list | https://w3c.github.io/vc-bitstring-status-list/contexts/v1.jsonld | -| | | | +| name | description | default value | +|---------------------------------|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| APPLICATION_PORT | port number of application | 8080 | +| APPLICATION_ENVIRONMENT | Environment of the application ie. local, dev, int and prod | local | +| DB_HOST | Database host | localhost | +| DB_PORT | Port of database | 5432 | +| DB_NAME | Database name | miw | +| USE_SSL | Whether SSL is enabled in database server | false | +| DB_USER_NAME | Database username | | +| DB_PASSWORD | Database password | | +| DB_POOL_SIZE | Max number of database connection acquired by application | 10 | +| KEYCLOAK_MIW_PUBLIC_CLIENT | Only needed if we want enable login with keyalock in swagger | miw_public | +| MANAGEMENT_PORT | Spring actuator port | 8090 | +| MIW_HOST_NAME | Application host name, this will be used in creation of did ie. did:web:MIW_HOST_NAME:BPN | localhost | +| ENCRYPTION_KEY | encryption key used to encrypt and decrypt private and public key of wallet | | +| AUTHORITY_WALLET_BPN | base wallet BPN number | BPNL000000000000 | +| AUTHORITY_WALLET_NAME | Base wallet name | Catena-X | +| AUTHORITY_WALLET_DID | Base wallet web did | web:did:host:BPNL000000000000 | +| VC_SCHEMA_LINK | Comma separated list of VC schema URL | https://www.w3.org/2018/credentials/v1, https://catenax-ng.github.io/product-core-schemas/businessPartnerData.json | +| VC_EXPIRY_DATE | Expiry date of VC (dd-MM-yyyy ie. 01-01-2025 expiry date will be 2024-12-31T18:30:00Z in VC) | 01-01-2025 | +| KEYCLOAK_REALM | Realm name of keycloak | miw_test | +| KEYCLOAK_CLIENT_ID | Keycloak private client id | | +| AUTH_SERVER_URL | Keycloak server url | | +| SUPPORTED_FRAMEWORK_VC_TYPES | Supported framework VC, provide values ie type1=value1,type2=value2 | cx-behavior-twin=Behavior Twin,cx-pcf=PCF,cx-quality=Quality,cx-resiliency=Resiliency,cx-sustainability=Sustainability,cx-traceability=ID_3.0_Trace | +| ENFORCE_HTTPS_IN_DID_RESOLUTION | Enforce https during web did resolution | true | +| CONTRACT_TEMPLATES_URL | Contract templates URL used in summary VC | https://public.catena-x.org/contracts/ | +| APP_LOG_LEVEL | Log level of application | INFO | +| AUTHORITY_SIGNING_SERVICE_TYPE | Base wallet signing type, Currency only LOCAL is supported | Local | +| LOCAL_SIGNING_KEY_STORAGE_TYPE | Key storage type, currently only DB is supported | DB | +| STATUS_LIST_2021_CONTEXT_URL | Context URI for status list 2021 | https://w3id.org/vc/status-list/2021/v1 | +| | | | # Technical Debts and Known issue diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java index 43b2a3a1..78c418f1 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/RevocationSettings.java @@ -26,5 +26,5 @@ import java.net.URI; @ConfigurationProperties(prefix = "miw.revocation") -public record RevocationSettings(URI url, URI bitStringStatusListContext) { +public record RevocationSettings(URI url, URI statusList2021Context) { } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java index db21186e..5de2569d 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/HoldersCredentialService.java @@ -199,8 +199,8 @@ public CredentialsResponse issueCredential(Map data, String call //add revocation context if missing List uris = verifiableCredential.getContext(); - if (!uris.contains(revocationSettings.bitStringStatusListContext())) { - uris.add(revocationSettings.bitStringStatusListContext()); + if (!uris.contains(revocationSettings.statusList2021Context())) { + uris.add(revocationSettings.statusList2021Context()); builder.contexts(uris); } diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java index fbdf58c6..6ed7a757 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/service/IssuersCredentialService.java @@ -233,8 +233,8 @@ public CredentialsResponse issueCredentialUsingBaseWallet(String holderDid, Map< //add revocation context if missing List uris = miwSettings.vcContexts(); - if (!uris.contains(revocationSettings.bitStringStatusListContext())) { - uris.add(revocationSettings.bitStringStatusListContext()); + if (!uris.contains(revocationSettings.statusList2021Context())) { + uris.add(revocationSettings.statusList2021Context()); builder.contexts(uris); } diff --git a/miw/src/main/resources/application.yaml b/miw/src/main/resources/application.yaml index c40961bb..5f86542d 100644 --- a/miw/src/main/resources/application.yaml +++ b/miw/src/main/resources/application.yaml @@ -110,7 +110,7 @@ miw: refresh-token-url: ${miw.security.token-url} revocation: url: ${REVOCATION_SERVICE_URL:http://localhost:8081} - bitStringStatusListContext: ${BITSTRING_STATUS_LIST_CONTEXT_URL:https://w3c.github.io/vc-bitstring-status-list/contexts/v1.jsonld} + statusList2021Context: ${STATUS_LIST_2021_CONTEXT_URL:https://w3id.org/vc/status-list/2021/v1} sts: diff --git a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java index 7e9e6289..39003371 100644 --- a/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java +++ b/miw/src/test/java/org/eclipse/tractusx/managedidentitywallets/utils/TestUtils.java @@ -135,7 +135,7 @@ public static void checkVC(VerifiableCredential verifiableCredential, MIWSetting } if (verifiableCredential.getVerifiableCredentialStatus() != null) { - Assertions.assertTrue(verifiableCredential.getContext().contains(revocationSettings.bitStringStatusListContext())); + Assertions.assertTrue(verifiableCredential.getContext().contains(revocationSettings.statusList2021Context())); } //check expiry date Assertions.assertEquals(0, verifiableCredential.getExpirationDate().compareTo(miwSettings.vcExpiryDate().toInstant())); diff --git a/revocation-service/src/main/resources/application.yaml b/revocation-service/src/main/resources/application.yaml index 35065c0b..3da1057f 100644 --- a/revocation-service/src/main/resources/application.yaml +++ b/revocation-service/src/main/resources/application.yaml @@ -50,7 +50,7 @@ revocation: refresh-token-url: ${revocation.security.keycloak.token-url} miw: url: ${MIW_URL:https://a888-203-129-213-107.ngrok-free.app} - vcContexts: ${VC_SCHEMA_LINK:https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json} + vcContexts: ${VC_SCHEMA_LINK:https://www.w3.org/2018/credentials/v1, https://w3id.org/vc/status-list/2021/v1} domain: url: ${DOMAIN_URL:https://977d-203-129-213-107.ngrok-free.app} From 3d0fbf9d18be9f3078e9c427a8f3239ae5a67b53 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Wed, 11 Sep 2024 12:03:35 +0530 Subject: [PATCH 51/60] fix: chart workflows --- .github/workflows/chart-verification.yml | 2 + .github/workflows/dast-scan.yaml | 1 + .github/workflows/release-miw.yml | 495 +++++++++--------- .github/workflows/release-revocation.yml | 495 +++++++++--------- charts/managed-identity-wallet/Chart.lock | 2 +- charts/managed-identity-wallet/README.md | 350 +++++++------ .../tests/custom-values/deployment_test.yaml | 2 +- .../tests/default/deployment_test.yaml | 2 +- charts/managed-identity-wallet/values.yaml | 108 ++-- 9 files changed, 740 insertions(+), 717 deletions(-) diff --git a/.github/workflows/chart-verification.yml b/.github/workflows/chart-verification.yml index 47bafb3a..07347afe 100644 --- a/.github/workflows/chart-verification.yml +++ b/.github/workflows/chart-verification.yml @@ -156,6 +156,7 @@ jobs: context: . push: true tags: kind-registry:5000/miw:testing + file: ./miw/Dockerfile - uses: actions/setup-python@v4 with: @@ -230,6 +231,7 @@ jobs: charts/managed-identity-wallet \ -n apps \ --wait \ + --timeout 10m \ --set image.tag=testing \ --set image.repository=kind-registry:5000/miw # only run if this is not a PR -OR- if there are new versions available diff --git a/.github/workflows/dast-scan.yaml b/.github/workflows/dast-scan.yaml index 24364172..afe6a25f 100644 --- a/.github/workflows/dast-scan.yaml +++ b/.github/workflows/dast-scan.yaml @@ -77,6 +77,7 @@ jobs: context: . push: true tags: kind-registry:5000/miw:testing + file: ./miw/Dockerfile - name: Install the chart on KinD cluster run: helm install -n apps --create-namespace --wait --set image.tag=testing --set=image.repository=kind-registry:5000/miw testing charts/managed-identity-wallet diff --git a/.github/workflows/release-miw.yml b/.github/workflows/release-miw.yml index 2e79820a..358984fe 100644 --- a/.github/workflows/release-miw.yml +++ b/.github/workflows/release-miw.yml @@ -16,250 +16,251 @@ # SPDX-License-Identifier: Apache-2.0 --- -name: Semantic Release - MIW -on: - push: - paths: - - 'miw/src/**' - - 'miw/build.gradle/**' - - 'wallet-commons/src/**' - - 'build.gradle' - - 'gradle.properties' - - 'settings.gradle' - branches: - - main - - develop - pull_request: - paths: - - 'miw/src/**' - - 'miw/build.gradle/**' - - 'wallet-commons/src/**' - - 'build.gradle' - - 'gradle.properties' - - 'settings.gradle' - branches: - - main - - develop - -env: - IMAGE_NAMESPACE: "tractusx" - IMAGE_NAME: "managed-identity-wallet" - -jobs: - - semantic_release: - name: Repository Release - runs-on: ubuntu-latest - permissions: - # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs - contents: write - pull-requests: write - packages: write - outputs: - next_release: ${{ steps.semantic-release.outputs.next_release }} - will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2 - - - name: Setup Helm - uses: azure/setup-helm@v4.1.0 - - - name: Setup JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - # setup helm-docs as it is needed during semantic-release - - uses: gabe565/setup-helm-docs-action@v1 - name: Setup helm-docs - if: github.event_name != 'pull_request' - with: - version: v1.11.3 - - - name: Run semantic release - id: semantic-release - if: github.event_name != 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com - GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com - run: | - npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release - - - name: Run semantic release (dry run) - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com - GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com - run: | - npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run - - - name: Execute Gradle build - run: ./gradlew build - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: build - path: ./miw/build - if-no-files-found: error - retention-days: 1 - - - name: Upload Helm chart artifact - uses: actions/upload-artifact@v4 - with: - name: charts - path: ./charts - if-no-files-found: error - retention-days: 1 - - - name: Report semantic-release outputs - run: | - echo "::notice::${{ env.next_release }}" - echo "::notice::${{ env.will_create_new_release }}" - - - name: Upload jar to GitHub release - if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} - run: | - echo "::notice::Uploading jar to GitHub release" - gh release upload "v$RELEASE_VERSION" ./miw/build/libs/miw-latest.jar - - docker: - name: Docker Release - needs: semantic_release - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: build - path: ./miw/build - - - name: Download Helm chart artifact - uses: actions/download-artifact@v4 - with: - name: charts - path: ./charts - - # Create SemVer or ref tags dependent of trigger event - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} - # Automatically prepare image tags; See action docs for more examples. - # semver patter will generate tags like these for example :1 :1.2 :1.2.3 - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} - type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} - type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} - - - name: DockerHub login - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - # Use existing DockerHub credentials present as secrets - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Push image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - file: ./miw/Dockerfile - - # https://github.com/peter-evans/dockerhub-description - # Important step to push image description to DockerHub - - name: Update Docker Hub description - if: github.event_name != 'pull_request' - uses: peter-evans/dockerhub-description@v3 - with: - # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' - readme-filepath: Docker-hub-notice.md - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} - - helm: - name: Helm Release - needs: semantic_release - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download Helm chart artifact - uses: actions/download-artifact@v4 - with: - name: charts - path: ./charts - - - name: Install Helm - uses: azure/setup-helm@v4.1.0 - - - name: Add Helm dependency repositories - run: | - helm repo add bitnami https://charts.bitnami.com/bitnami - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - - name: Release chart - if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' - run: | - # Package MIW chart - helm_package_path=$(helm package -u -d helm-charts ./charts/managed-identity-wallet | grep -o 'to: .*' | cut -d' ' -f2-) - echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV - - # Commit and push to gh-pages - git add helm-charts - git stash -- helm-charts - git reset --hard - git fetch origin - git checkout gh-pages - git stash pop - - # Generate helm repo index.yaml - helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ - git add index.yaml - - git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" - - git push origin gh-pages - - - name: Upload chart to GitHub release - if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} - HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} - run: | - echo "::notice::Uploading chart to GitHub release" - gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" + name: Semantic Release - MIW + on: + push: + paths: + - 'miw/src/**' + - 'miw/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' + branches: + - main + - develop + pull_request: + paths: + - 'miw/src/**' + - 'miw/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' + branches: + - main + - develop + + env: + IMAGE_NAMESPACE: "tractusx" + IMAGE_NAME: "managed-identity-wallet" + + jobs: + + semantic_release: + name: Repository Release + runs-on: ubuntu-latest + permissions: + # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + contents: write + pull-requests: write + packages: write + outputs: + next_release: ${{ steps.semantic-release.outputs.next_release }} + will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Setup Helm + uses: azure/setup-helm@v4.1.0 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # setup helm-docs as it is needed during semantic-release + - uses: gabe565/setup-helm-docs-action@v1 + name: Setup helm-docs + if: github.event_name != 'pull_request' + with: + version: v1.11.3 + + - name: Run semantic release + id: semantic-release + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release + + - name: Run semantic release (dry run) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run + + - name: Execute Gradle build + run: ./gradlew build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: build + path: ./miw/build + if-no-files-found: error + retention-days: 1 + + - name: Upload Helm chart artifact + uses: actions/upload-artifact@v4 + with: + name: charts + path: ./charts + if-no-files-found: error + retention-days: 1 + + - name: Report semantic-release outputs + run: | + echo "::notice::${{ env.next_release }}" + echo "::notice::${{ env.will_create_new_release }}" + + - name: Upload jar to GitHub release + if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} + run: | + echo "::notice::Uploading jar to GitHub release" + gh release upload "v$RELEASE_VERSION" ./miw/build/libs/miw-latest.jar + + docker: + name: Docker Release + needs: semantic_release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build + path: ./miw/build + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + # Create SemVer or ref tags dependent of trigger event + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + # Automatically prepare image tags; See action docs for more examples. + # semver patter will generate tags like these for example :1 :1.2 :1.2.3 + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: DockerHub login + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + # Use existing DockerHub credentials present as secrets + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Push image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ./miw/Dockerfile + + # https://github.com/peter-evans/dockerhub-description + # Important step to push image description to DockerHub + - name: Update Docker Hub description + if: github.event_name != 'pull_request' + uses: peter-evans/dockerhub-description@v3 + with: + # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' + readme-filepath: Docker-hub-notice.md + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + + helm: + name: Helm Release + needs: semantic_release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + - name: Install Helm + uses: azure/setup-helm@v4.1.0 + + - name: Add Helm dependency repositories + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Release chart + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + run: | + # Package MIW chart + helm_package_path=$(helm package -u -d helm-charts ./charts/managed-identity-wallet | grep -o 'to: .*' | cut -d' ' -f2-) + echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV + + # Commit and push to gh-pages + git add helm-charts + git stash -- helm-charts + git reset --hard + git fetch origin + git checkout gh-pages + git stash pop + + # Generate helm repo index.yaml + helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ + git add index.yaml + + git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" + + git push origin gh-pages + + - name: Upload chart to GitHub release + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} + HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} + run: | + echo "::notice::Uploading chart to GitHub release" + gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" + \ No newline at end of file diff --git a/.github/workflows/release-revocation.yml b/.github/workflows/release-revocation.yml index 2eeafee7..3a95fa1c 100644 --- a/.github/workflows/release-revocation.yml +++ b/.github/workflows/release-revocation.yml @@ -16,250 +16,251 @@ # SPDX-License-Identifier: Apache-2.0 --- -name: Semantic Release - Revocation Service -on: - push: - paths: - - 'revocation-service/src/**' - - 'revocation-service/build.gradle/**' - - 'wallet-commons/src/**' - - 'build.gradle' - - 'gradle.properties' - - 'settings.gradle' - branches: - - main - - develop - pull_request: - paths: - - 'revocation-service/src/**' - - 'revocation-service/build.gradle/**' - - 'wallet-commons/src/**' - - 'build.gradle' - - 'gradle.properties' - - 'settings.gradle' - branches: - - main - - develop - -env: - IMAGE_NAMESPACE: "tractusx" - IMAGE_NAME: "credential-revocation-service" - -jobs: - - semantic_release: - name: Repository Release - runs-on: ubuntu-latest - permissions: - # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs - contents: write - pull-requests: write - packages: write - outputs: - next_release: ${{ steps.semantic-release.outputs.next_release }} - will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2 - - - name: Setup Helm - uses: azure/setup-helm@v4.1.0 - - - name: Setup JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - # setup helm-docs as it is needed during semantic-release - - uses: gabe565/setup-helm-docs-action@v1 - name: Setup helm-docs - if: github.event_name != 'pull_request' - with: - version: v1.11.3 - - - name: Run semantic release - id: semantic-release - if: github.event_name != 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com - GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com - run: | - npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release - - - name: Run semantic release (dry run) - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com - GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com - run: | - npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run - - - name: Execute Gradle build - run: ./gradlew build - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: build - path: ./revocation-service/build - if-no-files-found: error - retention-days: 1 - - - name: Upload Helm chart artifact - uses: actions/upload-artifact@v4 - with: - name: charts - path: ./charts - if-no-files-found: error - retention-days: 1 - - - name: Report semantic-release outputs - run: | - echo "::notice::${{ env.next_release }}" - echo "::notice::${{ env.will_create_new_release }}" - - - name: Upload jar to GitHub release - if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} - run: | - echo "::notice::Uploading jar to GitHub release" - gh release upload "v$RELEASE_VERSION" ./revocation-service/build/libs/revocation-service-latest.jar - - docker: - name: Docker Release - needs: semantic_release - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: build - path: ./revocation-service/build - - - name: Download Helm chart artifact - uses: actions/download-artifact@v4 - with: - name: charts - path: ./charts - - # Create SemVer or ref tags dependent of trigger event - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} - # Automatically prepare image tags; See action docs for more examples. - # semver patter will generate tags like these for example :1 :1.2 :1.2.3 - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} - type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} - type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} - - - name: DockerHub login - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - # Use existing DockerHub credentials present as secrets - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Push image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - file: ./revocation-service/Dockerfile - - # https://github.com/peter-evans/dockerhub-description - # Important step to push image description to DockerHub - - name: Update Docker Hub description - if: github.event_name != 'pull_request' - uses: peter-evans/dockerhub-description@v3 - with: - # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' - readme-filepath: Docker-hub-notice.md - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} - - helm: - name: Helm Release - needs: semantic_release - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download Helm chart artifact - uses: actions/download-artifact@v4 - with: - name: charts - path: ./charts - - - name: Install Helm - uses: azure/setup-helm@v4.1.0 - - - name: Add Helm dependency repositories - run: | - helm repo add bitnami https://charts.bitnami.com/bitnami - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - - name: Release chart - if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' - run: | - # Package Revocation-service chart,this will not work as we do not have any chart there - helm_package_path=$(helm package -u -d helm-charts ./charts/revocation-service | grep -o 'to: .*' | cut -d' ' -f2-) - echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV - - # Commit and push to gh-pages - git add helm-charts - git stash -- helm-charts - git reset --hard - git fetch origin - git checkout gh-pages - git stash pop - - # Generate helm repo index.yaml - helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ - git add index.yaml - - git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" - - git push origin gh-pages - - - name: Upload chart to GitHub release - if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} - HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} - run: | - echo "::notice::Uploading chart to GitHub release" - gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" + name: Semantic Release - Revocation Service + on: + push: + paths: + - 'revocation-service/src/**' + - 'revocation-service/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' + branches: + - main + - develop + pull_request: + paths: + - 'revocation-service/src/**' + - 'revocation-service/build.gradle/**' + - 'wallet-commons/src/**' + - 'build.gradle' + - 'gradle.properties' + - 'settings.gradle' + branches: + - main + - develop + + env: + IMAGE_NAMESPACE: "tractusx" + IMAGE_NAME: "credential-revocation-service" + + jobs: + + semantic_release: + name: Repository Release + runs-on: ubuntu-latest + permissions: + # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + contents: write + pull-requests: write + packages: write + outputs: + next_release: ${{ steps.semantic-release.outputs.next_release }} + will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Setup Helm + uses: azure/setup-helm@v4.1.0 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # setup helm-docs as it is needed during semantic-release + - uses: gabe565/setup-helm-docs-action@v1 + name: Setup helm-docs + if: github.event_name != 'pull_request' + with: + version: v1.11.3 + + - name: Run semantic release + id: semantic-release + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release + + - name: Run semantic release (dry run) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run + + - name: Execute Gradle build + run: ./gradlew build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: build + path: ./revocation-service/build + if-no-files-found: error + retention-days: 1 + + - name: Upload Helm chart artifact + uses: actions/upload-artifact@v4 + with: + name: charts + path: ./charts + if-no-files-found: error + retention-days: 1 + + - name: Report semantic-release outputs + run: | + echo "::notice::${{ env.next_release }}" + echo "::notice::${{ env.will_create_new_release }}" + + - name: Upload jar to GitHub release + if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} + run: | + echo "::notice::Uploading jar to GitHub release" + gh release upload "v$RELEASE_VERSION" ./revocation-service/build/libs/revocation-service-latest.jar + + docker: + name: Docker Release + needs: semantic_release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build + path: ./revocation-service/build + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + # Create SemVer or ref tags dependent of trigger event + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + # Automatically prepare image tags; See action docs for more examples. + # semver patter will generate tags like these for example :1 :1.2 :1.2.3 + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: DockerHub login + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + # Use existing DockerHub credentials present as secrets + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Push image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + file: ./revocation-service/Dockerfile + + # https://github.com/peter-evans/dockerhub-description + # Important step to push image description to DockerHub + - name: Update Docker Hub description + if: github.event_name != 'pull_request' + uses: peter-evans/dockerhub-description@v3 + with: + # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' + readme-filepath: Docker-hub-notice.md + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + + helm: + name: Helm Release + needs: semantic_release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + - name: Install Helm + uses: azure/setup-helm@v4.1.0 + + - name: Add Helm dependency repositories + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Release chart + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + run: | + # Package Revocation-service chart,this will not work as we do not have any chart there + helm_package_path=$(helm package -u -d helm-charts ./charts/revocation-service | grep -o 'to: .*' | cut -d' ' -f2-) + echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV + + # Commit and push to gh-pages + git add helm-charts + git stash -- helm-charts + git reset --hard + git fetch origin + git checkout gh-pages + git stash pop + + # Generate helm repo index.yaml + helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ + git add index.yaml + + git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" + + git push origin gh-pages + + - name: Upload chart to GitHub release + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} + HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} + run: | + echo "::notice::Uploading chart to GitHub release" + gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" + \ No newline at end of file diff --git a/charts/managed-identity-wallet/Chart.lock b/charts/managed-identity-wallet/Chart.lock index 2fd40018..259b055b 100644 --- a/charts/managed-identity-wallet/Chart.lock +++ b/charts/managed-identity-wallet/Chart.lock @@ -12,4 +12,4 @@ dependencies: repository: file://charts/pgadmin4 version: 1.19.0 digest: sha256:886b90f763f2320a1601e15b06264065a764f51fc34d592c0f0a08bd76f01635 -generated: "2024-08-22T18:04:25.649769241+05:30" +generated: "2024-09-11T11:53:55.835418982+05:30" diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index fb5ba79c..c157e1e5 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -1,6 +1,6 @@ -# Managed Identity Wallet - Verifiable Credential Revocation Service +# managed-identity-wallet ![Version: 1.0.0-develop.4](https://img.shields.io/badge/Version-1.0.0--develop.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.4](https://img.shields.io/badge/AppVersion-1.0.0--develop.4-informational?style=flat-square) @@ -41,9 +41,9 @@ And at the same it shall support an uninterrupted tracking and tracing and docum ### Install Chart - helm install [RELEASE_NAME] charts/managed-identity-wallet + helm install [RELEASE_NAME] tractusx-dev/managed-identity-wallet - #This will spin up the container for Managed Identity Wallet application, Verifiable Credential Revocation Service application, Keycloak and Postgresql + helm install [RELEASE_NAME] tractusx-stable/managed-identity-wallet

(back to top)

@@ -75,179 +75,183 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document ## Requirements -| Repository | Name | Version | -| ---------------------------------- | ---------- | ------- | -| file://charts/pgadmin4 | pgadmin4 | 1.19.0 | -| https://charts.bitnami.com/bitnami | common | 2.x.x | -| https://charts.bitnami.com/bitnami | keycloak | 15.1.6 | +| Repository | Name | Version | +|------------|------|---------| +| file://charts/pgadmin4 | pgadmin4 | 1.19.0 | +| https://charts.bitnami.com/bitnami | common | 2.x.x | +| https://charts.bitnami.com/bitnami | keycloak | 15.1.6 | | https://charts.bitnami.com/bitnami | postgresql | 11.9.13 |

(back to top)

## Values -| Key | Type | Default | Description | -| ------------------------------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| affinity | object | `{}` | Affinity configuration | -| envs | object | `{}` | envs Parameters for the application (will be provided as environment variables) | -| extraVolumeMounts | list | `[]` | add volume mounts to the miw deployment | -| extraVolumes | list | `[]` | add volumes to the miw deployment | -| fullnameOverride | string | `""` | String to fully override common.names.fullname template | -| image.pullPolicy | string | `"Always"` | PullPolicy | -| image.repository | string | `"tractusx/managed-identity-wallet"` | Image repository | -| image.tag | string | `""` | Image tag (empty one will use "appVersion" value from chart definition) | -| ingress.annotations | object | `{}` | Ingress annotations | -| ingress.enabled | bool | `false` | Enable ingress controller resource | -| ingress.hosts | list | `[]` | Ingress accepted hostnames | -| ingress.tls | list | `[]` | Ingress TLS configuration | -| initContainers | list | `[]` | add initContainers to the miw deployment | -| keycloak.auth.adminPassword | string | `""` | Keycloak admin password | -| keycloak.auth.adminUser | string | `"admin"` | Keycloak admin user | -| keycloak.enabled | bool | `true` | Enable to deploy Keycloak | -| keycloak.extraEnvVars | list | `[]` | Extra environment variables | -| keycloak.ingress.annotations | object | `{}` | | -| keycloak.ingress.enabled | bool | `false` | | -| keycloak.ingress.hosts | list | `[]` | | -| keycloak.ingress.tls | list | `[]` | | -| keycloak.keycloakConfigCli.backoffLimit | int | `2` | Number of retries before considering a Job as failed | -| keycloak.keycloakConfigCli.enabled | bool | `true` | Enable to create the miw playground realm | -| keycloak.keycloakConfigCli.existingConfigmap | string | `"keycloak-realm-config"` | Existing configmap name for the realm configuration | -| keycloak.postgresql.auth.database | string | `"miw_keycloak"` | Database name | -| keycloak.postgresql.auth.password | string | `""` | KeycloakPostgresql password to set (if empty one is generated) | -| keycloak.postgresql.auth.username | string | `"miw_keycloak"` | Keycloak PostgreSQL user | -| keycloak.postgresql.enabled | bool | `true` | Enable to deploy PostgreSQL | -| keycloak.postgresql.nameOverride | string | `"keycloak-postgresql"` | Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. | -| livenessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":20,"periodSeconds":5,"timeoutSeconds":15}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | -| livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe at all | -| livenessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | -| livenessProbe.initialDelaySeconds | int | `20` | Number of seconds after the container has started before readiness probe are initiated. | -| livenessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | -| livenessProbe.timeoutSeconds | int | `15` | Number of seconds after which the probe times out. | -| miw.authorityWallet.bpn | string | `"BPNL000000000000"` | Authority Wallet BPNL | -| miw.authorityWallet.name | string | `""` | Authority Wallet Name | -| miw.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | -| miw.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | -| miw.database.encryptionKey.value | string | `""` | Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. | -| miw.database.host | string | `"{{ .Release.Name }}-postgresql"` | Database host | -| miw.database.name | string | `"miw_app"` | Database name | -| miw.database.port | int | `5432` | Database port | -| miw.database.secret | string | `"{{ .Release.Name }}-postgresql"` | Existing secret name for the database password | -| miw.database.secretPasswordKey | string | `""` | Existing secret key for the database password | -| miw.database.useSSL | bool | `false` | Set to true to enable SSL connection to the database | -| miw.database.user | string | `"miw"` | Database user | -| miw.environment | string | `"dev"` | Runtime environment. Should be ether local, dev, int or prod | -| miw.host | string | `"{{ .Release.Name }}-managed-identity-wallet:8080"` | Host name | -| miw.keycloak.clientId | string | `"miw_private_client"` | Keycloak client id | -| miw.keycloak.realm | string | `"miw_test"` | Keycloak realm | -| miw.keycloak.url | string | `"http://{{ .Release.Name }}-keycloak"` | Keycloak URL | -| miw.logging.level | string | `"INFO"` | Log level. Should be ether ERROR, WARN, INFO, DEBUG, or TRACE. | -| miw.ssi.enforceHttpsInDidWebResolution | bool | `true` | Enable to use HTTPS in DID Web Resolution | -| miw.ssi.vcExpiryDate | string | `""` | Verifiable Credential expiry date. Format 'dd-MM-yyyy'. If empty it is set to 31-12- | -| nameOverride | string | `""` | String to partially override common.names.fullname template (will maintain the release name) | -| networkPolicy.enabled | bool | `false` | If `true` network policy will be created to restrict access to managed-identity-wallet | -| networkPolicy.from | list | `[{"namespaceSelector":{}}]` | Specify from rule network policy for miw (defaults to all namespaces) | -| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector configuration | -| pgadmin4.enabled | bool | `false` | Enable to deploy pgAdmin | -| pgadmin4.env.email | string | `"admin@miw.com"` | Preset the admin user email | -| pgadmin4.env.password | string | `"very-secret-password"` | preset password (there is no auto-generated password) | -| pgadmin4.extraServerDefinitions.enabled | bool | `true` | enable the predefined server for pgadmin | -| pgadmin4.extraServerDefinitions.servers | object | `{}` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L84) how to configure the predefined servers | -| pgadmin4.ingress.annotations | object | `{}` | | -| pgadmin4.ingress.enabled | bool | `false` | Enagle pgAdmin ingress | -| pgadmin4.ingress.hosts | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L104) how to configure the ingress host(s) | -| pgadmin4.ingress.tls | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L109) how to configure tls for the ingress host(s) | -| podAnnotations | object | `{}` | PodAnnotation configuration | -| podSecurityContext | object | `{}` | PodSecurityContext | -| postgresql.auth.database | string | `"miw_app"` | Postgresql database to create | -| postgresql.auth.enablePostgresUser | bool | `false` | Enable postgresql admin user | -| postgresql.auth.password | string | `""` | Postgresql password to set (if empty one is generated) | -| postgresql.auth.postgresPassword | string | `""` | Postgresql admin user password | -| postgresql.auth.username | string | `"miw"` | Postgresql user to create | -| postgresql.backup.cronjob.schedule | string | `"* */6 * * *"` | Backup schedule | -| postgresql.backup.cronjob.storage.existingClaim | string | `""` | Name of an existing PVC to use | -| postgresql.backup.cronjob.storage.resourcePolicy | string | `"keep"` | Set resource policy to "keep" to avoid removing PVCs during a helm delete operation | -| postgresql.backup.cronjob.storage.size | string | `"8Gi"` | PVC Storage Request for the backup data volume | -| postgresql.backup.enabled | bool | `false` | Enable to create a backup cronjob | -| postgresql.enabled | bool | `true` | Enable to deploy Postgresql | -| readinessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":30,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":5}` | Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | -| readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe at all | -| readinessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | -| readinessProbe.initialDelaySeconds | int | `30` | Number of seconds after the container has started before readiness probe are initiated. | -| readinessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | -| readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed. | -| readinessProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | -| replicaCount | int | `1` | The amount of replicas to run | -| resources.limits.cpu | int | `2` | CPU resource limits | -| resources.limits.memory | string | `"1Gi"` | Memory resource limits | -| resources.requests.cpu | string | `"250m"` | CPU resource requests | -| resources.requests.memory | string | `"500Mi"` | Memory resource requests | -| secrets | object | `{}` | Parameters for the application (will be stored as secrets - so, for passwords, ...) | -| securityContext.allowPrivilegeEscalation | bool | `false` | Allow privilege escalation | -| securityContext.privileged | bool | `false` | Enable privileged container | -| securityContext.runAsGroup | int | `11111` | Group ID used to run the container | -| securityContext.runAsNonRoot | bool | `true` | Enable to run the container as a non-root user | -| securityContext.runAsUser | int | `11111` | User ID used to run the container | -| service.port | int | `8080` | Kubernetes Service port | -| service.type | string | `"ClusterIP"` | Kubernetes Service type | -| serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | -| serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | -| serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | -| tolerations | list | `[]` | Tolerations configuration | -| vcrs.replicaCount | int | `1` | Number of replicas to run | -| vcrs.url | string | `"https://example.com"` | Application URL | -| vcrs.vcContexts | string | `"https://www.w3.org/2018/credentials/v1, https://w3id.org/vc/status-list/2021/v1"` | App VC context | -| vcrs.domain.url | string | `"https://example.com"` | App domain | -| vcrs.domain.host | string | `"localhost"` | The application name | -| vcrs.appName | string | `"verifiable-credential-revocation-service"` | The configmap name | -| vcrs.appPort | string | `"8081"` | The application port | -| vcrs.appProfile | string | `"local"` | The application profile | -| vcrs.applicationLogLevel | string | `"DEBUG"` | The application log level | -| vcrs.configName | string | `"verifiable-credential-revocation-service-config"` | The service name | -| vcrs.serviceName | string | `"verifiable-credential-revocation-service"` | The secret name | -| vcrs.secretName | string | `"verifiable-credential-revocation-service-secret"` | The secret name | -| vcrs.ingressName | string | `"verifiable-credential-revocation-service-ingress"` | Ingress name | -| vcrs.image.repository | string | `"docker.io/example"` | Image repository | -| vcrs.image.pullPolicy | string | `"IfNotPresent"` | PullPolicy | -| vcrs.image.tag | string | `"latest"` | Image tag (empty one will use "appVersion" value from chart definition) | -| vcrs.resources.requests.cpu | string | `"250m"` | CPU resource requests | -| vcrs.resources.requests.memory | string | `"512Mi"` | Memory resource requests | -| vcrs.resources.limits.cpu | string | `"500m"` | CPU resource limits | -| vcrs.resources.limits.memory | string | `"1Gi"` | Memory resource limits | -| vcrs.livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe | -| vcrs.livenessProbe.failureThreshold | int | `5` | Failure threshold for liveness probe | -| vcrs.livenessProbe.initialDelaySeconds | int | `60` | Initial delay before liveness probe starts | -| vcrs.livenessProbe.timeoutSeconds | int | `30` | Timeout for liveness probe | -| vcrs.livenessProbe.periodSeconds | int | `15` | How often to perform liveness probe | -| vcrs.readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe | -| vcrs.readinessProbe.failureThreshold | int | `5` | Failure threshold for readiness probe | -| vcrs.readinessProbe.initialDelaySeconds | int | `60` | Initial delay before readiness probe starts | -| vcrs.readinessProbe.timeoutSeconds | int | `15` | Timeout for readiness probe | -| vcrs.readinessProbe.periodSeconds | int | `15` | How often to perform readiness probe | -| vcrs.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the readiness probe to be considered successful | -| vcrs.ingress.enabled | bool | `false` | Enable to deploy ingress | -| vcrs.ingress.tls | bool | `false` | TLS configuration for ingress | -| vcrs.ingress.urlPrefix | string | `/` | URL prefix for ingress | -| vcrs.ingress.className | string | `"nginx"` | Ingress class name | -| vcrs.ingress.annotations | object | `{}` | Ingress annotations | -| vcrs.ingress.service.type | string | `"ClusterIP"` | Kubernetes Service type | -| vcrs.ingress.service.port | int | `8081` | Kubernetes Service port | -| vcrs.database.databaseHost | string | `"managed-identity-wallet-postgresql"` | The Database Host | -| vcrs.database.databasePort | int | `5432` | The Database Port | -| vcrs.database.databaseName | string | `"vcrs_app"` | The Database Name | -| vcrs.database.databaseUseSSL | bool | `false` | The Database SSL | -| vcrs.database.databaseUsername | string | `"vcrs"` | The Database Username | -| vcrs.database.databaseConnectionPoolSize | int | `10` | The Database connection pool size | -| vcrs.database.databasepass | string | `""` | The Database password | -| vcrs.swagger.enableSwaggerUi | bool | `true` | Enable Swagger UI | -| vcrs.swagger.enableApiDoc | bool | `true` | Enable Swagger API Doc | -| vcrs.security.serviceSecurityEnabed | bool | `true` | Enable application security | -| vcrs.keycloak.enabled | bool | `false` | Enable Keycloak | -| vcrs.keycloak.keycloakRealm | string | `"miw_test"` | Keycloak Realm | -| vcrs.keycloak.clientId | string | `"miw_private_client"` | Keycloak Client ID | -| vcrs.keycloak.publicClientId | string | `"miw_public_client"` | Keycloak Public Client ID | -| vcrs.keycloak.authServerUrl | string | `"http://{{ .Release.Name }}-keycloak"` | Keycloak Auth Server URL | -| vcrs.logging.revocation | string | `"INFO"` | Logging method for revocation | - +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Affinity configuration | +| envs | object | `{}` | envs Parameters for the application (will be provided as environment variables) | +| extraVolumeMounts | list | `[]` | | +| extraVolumes | list | `[]` | add volumes to the miw deployment | +| fullnameOverride | string | `""` | String to fully override common.names.fullname template | +| image.pullPolicy | string | `"Always"` | PullPolicy | +| image.repository | string | `"tractusx/managed-identity-wallet"` | Image repository | +| image.tag | string | `""` | Image tag (empty one will use "appVersion" value from chart definition) | +| imagePullSecrets | list | `[]` | | +| ingress | object | `{"annotations":{},"className":"nginx","enabled":false,"hosts":[],"tls":[]}` | Ingress Configuration | +| ingress.annotations | object | `{}` | Ingress annotations | +| ingress.enabled | bool | `false` | Enable ingress controller resource | +| ingress.hosts | list | `[]` | Ingress accepted hostnames | +| ingress.tls | list | `[]` | Ingress TLS configuration | +| initContainers | list | `[]` | add initContainers to the miw deployment | +| keycloak | object | `{"auth":{"adminPassword":"","adminUser":"admin"},"enabled":true,"extraEnvVars":[],"ingress":{"annotations":{},"enabled":false,"hosts":[],"tls":[]},"keycloakConfigCli":{"backoffLimit":2,"enabled":true,"existingConfigmap":"keycloak-realm-config"},"postgresql":{"auth":{"database":"miw_keycloak","password":"defaultpassword","username":"miw_keycloak"},"enabled":true,"nameOverride":"keycloak-postgresql","volumePermissions":{"enabled":true}}}` | Values for KEYCLOAK | +| keycloak.auth.adminPassword | string | `""` | Keycloak admin password | +| keycloak.auth.adminUser | string | `"admin"` | Keycloak admin user | +| keycloak.enabled | bool | `true` | Enable to deploy Keycloak | +| keycloak.extraEnvVars | list | `[]` | Extra environment variables | +| keycloak.ingress.annotations | object | `{}` | Ingress annotations | +| keycloak.ingress.enabled | bool | `false` | Enable ingress controller resource | +| keycloak.ingress.hosts | list | `[]` | Ingress accepted hostnames | +| keycloak.ingress.tls | list | `[]` | Ingress TLS configuration | +| keycloak.keycloakConfigCli.backoffLimit | int | `2` | Number of retries before considering a Job as failed | +| keycloak.keycloakConfigCli.enabled | bool | `true` | Enable to create the miw playground realm | +| keycloak.keycloakConfigCli.existingConfigmap | string | `"keycloak-realm-config"` | Existing configmap name for the realm configuration | +| keycloak.postgresql.auth.database | string | `"miw_keycloak"` | Database name | +| keycloak.postgresql.auth.password | string | `"defaultpassword"` | KeycloakPostgresql password to set (if empty one is generated) | +| keycloak.postgresql.auth.username | string | `"miw_keycloak"` | Postgresql admin user password | +| keycloak.postgresql.enabled | bool | `true` | Enable to deploy PostgreSQL | +| keycloak.postgresql.nameOverride | string | `"keycloak-postgresql"` | Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. | +| miw | object | `{"authorityWallet":{"bpn":"BPNL000000000000","name":""},"database":{"encryptionKey":{"secret":"","secretKey":"","value":""},"host":"{{ .Release.Name }}-postgresql","name":"miw_app","port":5432,"secret":"verifiable-credential-revocation-service","secretPasswordKey":"password","useSSL":false,"user":"miw"},"environment":"dev","host":"{{ .Release.Name }}-managed-identity-wallet:8080","keycloak":{"clientId":"miw_private_client","realm":"miw_test","url":"http://{{ .Release.Name }}-keycloak"},"livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":20,"periodSeconds":5,"timeoutSeconds":15},"logging":{"level":"INFO"},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":30,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":5},"ssi":{"enforceHttpsInDidWebResolution":true,"vcExpiryDate":""}}` | Values for MIW | +| miw.authorityWallet.bpn | string | `"BPNL000000000000"` | Authority Wallet BPNL | +| miw.authorityWallet.name | string | `""` | Authority Wallet Name | +| miw.database.encryptionKey | object | `{"secret":"","secretKey":"","value":""}` | Password encryption configuratons | +| miw.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | +| miw.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | +| miw.database.encryptionKey.value | string | `""` | Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. | +| miw.database.host | string | `"{{ .Release.Name }}-postgresql"` | Database host | +| miw.database.name | string | `"miw_app"` | Database name | +| miw.database.port | int | `5432` | Database port | +| miw.database.secret | string | `"verifiable-credential-revocation-service"` | Existing secret name for the database password | +| miw.database.secretPasswordKey | string | `"password"` | Existing secret key for the database password | +| miw.database.useSSL | bool | `false` | Set to true to enable SSL connection to the database | +| miw.database.user | string | `"miw"` | Database user | +| miw.environment | string | `"dev"` | Runtime environment. Should be ether local, dev, int or prod | +| miw.host | string | `"{{ .Release.Name }}-managed-identity-wallet:8080"` | Host name | +| miw.keycloak.clientId | string | `"miw_private_client"` | Keycloak client id | +| miw.keycloak.realm | string | `"miw_test"` | Keycloak realm | +| miw.keycloak.url | string | `"http://{{ .Release.Name }}-keycloak"` | Keycloak URL | +| miw.livenessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":20,"periodSeconds":5,"timeoutSeconds":15}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | +| miw.livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe at all | +| miw.livenessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | +| miw.livenessProbe.initialDelaySeconds | int | `20` | Number of seconds after the container has started before readiness probe are initiated. | +| miw.livenessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | +| miw.livenessProbe.timeoutSeconds | int | `15` | Number of seconds after which the probe times out. | +| miw.logging.level | string | `"INFO"` | Log level. Should be ether ERROR, WARN, INFO, DEBUG, or TRACE. | +| miw.readinessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":30,"periodSeconds":5,"successThreshold":1,"timeoutSeconds":5}` | Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | +| miw.readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe at all | +| miw.readinessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | +| miw.readinessProbe.initialDelaySeconds | int | `30` | Number of seconds after the container has started before readiness probe are initiated. | +| miw.readinessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | +| miw.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed. | +| miw.readinessProbe.timeoutSeconds | int | `5` | Number of seconds after which the probe times out. | +| miw.ssi.enforceHttpsInDidWebResolution | bool | `true` | Enable to use HTTPS in DID Web Resolution | +| miw.ssi.vcExpiryDate | string | `""` | Verifiable Credential expiry date. Format 'dd-MM-yyyy'. If empty it is set to 31-12- | +| nameOverride | string | `""` | String to partially override common.names.fullname template (will maintain the release name) | +| networkPolicy.enabled | bool | `false` | If `true` network policy will be created to restrict access to managed-identity-wallet | +| networkPolicy.from | list | `[{"namespaceSelector":{}}]` | Specify from rule network policy for miw (defaults to all namespaces) | +| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector configuration | +| pgadmin4 | object | `{"enabled":false,"env":{"email":"admin@miw.com","password":"very-secret-password"},"extraServerDefinitions":{"enabled":true,"servers":{}},"ingress":{"annotations":{},"enabled":false,"hosts":[],"tls":[]}}` | Values for PGADMIN For more information on how to configure the pgadmin chart see https://artifacthub.io/packages/helm/runix/pgadmin4. | +| pgadmin4.enabled | bool | `false` | Enable to deploy pgAdmin | +| pgadmin4.env.email | string | `"admin@miw.com"` | Preset the admin user email | +| pgadmin4.env.password | string | `"very-secret-password"` | preset password (there is no auto-generated password) | +| pgadmin4.extraServerDefinitions.enabled | bool | `true` | enable the predefined server for pgadmin | +| pgadmin4.extraServerDefinitions.servers | object | `{}` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L84) how to configure the predefined servers | +| pgadmin4.ingress.enabled | bool | `false` | Enagle pgAdmin ingress | +| pgadmin4.ingress.hosts | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L104) how to configure the ingress host(s) | +| pgadmin4.ingress.tls | list | `[]` | See [here](https://github.com/rowanruseler/helm-charts/blob/9b970b2e419c2300dfbb3f827a985157098a0287/charts/pgadmin4/values.yaml#L109) how to configure tls for the ingress host(s) | +| podAnnotations | object | `{}` | PodAnnotation configuration | +| podSecurityContext | object | `{}` | Pod security configurations | +| postgresql | object | `{"auth":{"database":"miw_app","enablePostgresUser":true,"existingSecret":"verifiable-credential-revocation-service","username":"miw"},"backup":{"cronjob":{"schedule":"* */6 * * *","storage":{"existingClaim":"","resourcePolicy":"keep","size":"8Gi"}},"enabled":false},"enabled":true,"image":{"debug":true,"tag":"16-debian-12"},"primary":{"extraVolumeMounts":[{"mountPath":"/docker-entrypoint-initdb.d/seed","name":"postgres-seed"}],"extraVolumes":[{"name":"postgres-seed","persistentVolumeClaim":{"claimName":"postgres-seed-pvc"}}],"initdb":{"password":"defaultpassword","scripts":{"init.sql":"CREATE DATABASE vcrs_app;\nCREATE USER vcrs WITH ENCRYPTED PASSWORD 'defaultpassword';\nGRANT ALL PRIVILEGES ON DATABASE vcrs_app TO vcrs;\n\\c vcrs_app\nGRANT ALL ON SCHEMA public TO vcrs;\n"},"user":"postgres"}},"volumePermissions":{"enabled":true}}` | Values for POSTGRESQL For more information on how to configure the PostgreSQL chart see https://github.com/bitnami/charts/tree/main/bitnami/postgresql. | +| postgresql.auth.database | string | `"miw_app"` | Postgresql database to create | +| postgresql.auth.enablePostgresUser | bool | `true` | Enable postgresql admin user | +| postgresql.auth.existingSecret | string | `"verifiable-credential-revocation-service"` | Postgresql root-user and non-root user secret | +| postgresql.auth.username | string | `"miw"` | Postgresql user to create | +| postgresql.backup.cronjob | object | `{"schedule":"* */6 * * *","storage":{"existingClaim":"","resourcePolicy":"keep","size":"8Gi"}}` | Cronjob Configuration | +| postgresql.backup.cronjob.schedule | string | `"* */6 * * *"` | Backup schedule | +| postgresql.backup.cronjob.storage.existingClaim | string | `""` | Name of an existing PVC to use | +| postgresql.backup.cronjob.storage.resourcePolicy | string | `"keep"` | Set resource policy to "keep" to avoid removing PVCs during a helm delete operation | +| postgresql.backup.cronjob.storage.size | string | `"8Gi"` | PVC Storage Request for the backup data volume | +| postgresql.backup.enabled | bool | `false` | Enable to create a backup cronjob | +| postgresql.enabled | bool | `true` | Enable to deploy Postgresql | +| postgresql.image.debug | bool | `true` | Debug logs | +| replicaCount | int | `1` | The amount of replicas to run | +| resources.limits.cpu | int | `2` | CPU resource limits | +| resources.limits.memory | string | `"1Gi"` | Memory resource limits | +| resources.requests.cpu | string | `"250m"` | CPU resource requests | +| resources.requests.memory | string | `"500Mi"` | Memory resource requests | +| secrets | object | `{}` | Parameters for the application (will be stored as secrets - so, for passwords, ...) | +| securityContext | object | `{"allowPrivilegeEscalation":false,"privileged":false,"runAsGroup":11111,"runAsNonRoot":true,"runAsUser":11111}` | Pod security parameters | +| securityContext.allowPrivilegeEscalation | bool | `false` | Allow privilege escalation | +| securityContext.privileged | bool | `false` | Enable privileged container | +| securityContext.runAsGroup | int | `11111` | Group ID used to run the container | +| securityContext.runAsNonRoot | bool | `true` | Enable to run the container as a non-root user | +| securityContext.runAsUser | int | `11111` | User ID used to run the container | +| service.port | int | `8080` | Kubernetes Service port | +| service.type | string | `"ClusterIP"` | Kubernetes Service type | +| serviceAccount.annotations | object | `{}` | Annotations to add to the ServiceAccount | +| serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | +| serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | +| tolerations | list | `[]` | Tolerations configuration | +| vcrs | object | `{"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service","tag":"latest"},"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":5,"initialDelaySeconds":60,"periodSeconds":15,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","readinessProbe":{"enabled":true,"failureThreshold":5,"initialDelaySeconds":60,"periodSeconds":15,"successThreshold":1,"timeoutSeconds":15},"replicaCount":1,"resources":{"limits":{"cpu":"500m","memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"serviceName":"verifiable-credential-revocation-service"}` | Values for Verifiable Credential Revocation Service application | +| vcrs.configName | string | `"verifiable-credential-revocation-service"` | ConfigMap Name | +| vcrs.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | +| vcrs.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | +| vcrs.database.encryptionKey.value | string | `""` | Database encryption key for confidential data. Ignored if `secret` is set. If empty a secret with 32 random alphanumeric chars is generated. | +| vcrs.env.APPLICATION_LOG_LEVEL | string | `"DEBUG"` | The application log level | +| vcrs.env.APPLICATION_NAME | string | `"verifiable-credential-revocation-service"` | The application name | +| vcrs.env.APPLICATION_PORT | int | `8081` | The application port | +| vcrs.env.APPLICATION_PROFILE | string | `"local"` | The application profile | +| vcrs.env.AUTH_SERVER_URL | string | `"http://{{ .Release.Name }}-keycloak"` | Auth URL for Keycloak | +| vcrs.env.DATABASE_CONNECTION_POOL_SIZE | int | `10` | The Database connection pool size | +| vcrs.env.DATABASE_HOST | string | `"managed-identity-wallet-postgresql"` | The Database Host | +| vcrs.env.DATABASE_NAME | string | `"vcrs_app"` | The Database Name | +| vcrs.env.DATABASE_PORT | int | `5432` | The Database Port | +| vcrs.env.DATABASE_USERNAME | string | `"vcrs"` | The Database Name | +| vcrs.env.DATABASE_USE_SSL_COMMUNICATION | bool | `false` | The Database SSL | +| vcrs.env.ENABLE_API_DOC | bool | `true` | Swagger Api Doc | +| vcrs.env.ENABLE_SWAGGER_UI | bool | `true` | Swagger UI config | +| vcrs.env.KEYCLOAK_CLIENT_ID | string | `"miw_private_client"` | ClientID Config | +| vcrs.env.KEYCLOAK_PUBLIC_CLIENT_ID | string | `"miw_public_client"` | ClientID Config | +| vcrs.env.KEYCLOAK_REALM | string | `"miw_test"` | KeyClocak Configurations | +| vcrs.env.MIW_URL | string | `"https://a888-203-129-213-107.ngrok-free.app"` | Revocation application configuration | +| vcrs.fullnameOverride | string | `"verifiable-credential-revocation-service"` | String to partially override common.names.fullname template (will maintain the release name) | +| vcrs.host | string | `"localhost"` | Revocation application configuration | +| vcrs.image.pullPolicy | string | `"IfNotPresent"` | PullPolicy | +| vcrs.image.repository | string | `"public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service"` | Image repository | +| vcrs.image.tag | string | `"latest"` | Image tag (empty one will use "appVersion" value from chart definition) | +| vcrs.ingress.service.port | int | `8081` | Kubernetes Service port | +| vcrs.ingress.service.type | string | `"ClusterIP"` | Kubernetes Service type | +| vcrs.livenessProbe | object | `{"enabled":true,"failureThreshold":5,"initialDelaySeconds":60,"periodSeconds":15,"timeoutSeconds":30}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | +| vcrs.livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe at all | +| vcrs.livenessProbe.failureThreshold | int | `5` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | +| vcrs.livenessProbe.initialDelaySeconds | int | `60` | Number of seconds after the container has started before readiness probes are initiated. | +| vcrs.livenessProbe.periodSeconds | int | `15` | How often (in seconds) to perform the probe | +| vcrs.livenessProbe.timeoutSeconds | int | `30` | Number of seconds after which the probe times out. | +| vcrs.nameOverride | string | `"verifiable-credential-revocation-service"` | The configmap name | +| vcrs.readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe at all | +| vcrs.readinessProbe.failureThreshold | int | `5` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | +| vcrs.readinessProbe.initialDelaySeconds | int | `60` | Number of seconds after the container has started before readiness probe are initiated. | +| vcrs.readinessProbe.periodSeconds | int | `15` | How often (in seconds) to perform the probe | +| vcrs.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed. | +| vcrs.readinessProbe.timeoutSeconds | int | `15` | Number of seconds after which the probe times out. | +| vcrs.resources.limits.cpu | string | `"500m"` | CPU resource limits | +| vcrs.resources.limits.memory | string | `"1Gi"` | Memory resource limits | +| vcrs.resources.requests.cpu | string | `"250m"` | CPU resource requests | +| vcrs.resources.requests.memory | string | `"512Mi"` | Memory resource requests | +| vcrs.secretName | string | `"verifiable-credential-revocation-service"` | The Secret name | +| vcrs.secrets.DATABASE_PASSWORD | string | `"defaultpassword"` | The Database Password | +| vcrs.secrets.password | string | `"defaultpassword"` | Postgresql password for MIW non-root User | +| vcrs.secrets.postgres-password | string | `"defaultpassword"` | Postgresql password for postgres root-user | +| vcrs.serviceName | string | `"verifiable-credential-revocation-service"` | The Service name | For more information on how to configure the Keycloak see - https://github.com/bitnami/charts/tree/main/bitnami/keycloak. @@ -315,10 +319,10 @@ when deploying the MIW in a production environment: ## Maintainers -| Name | Email | Url | -| -------------- | ---------------------------------- | ---------------------------------- | +| Name | Email | Url | +| ---- | ------ | --- | | Dominik Pinsel | | | -| Rohit Solanki | | | +| Rohit Solanki | | |

(back to top)

diff --git a/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml b/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml index 029f0e0a..cca4627a 100644 --- a/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml +++ b/charts/managed-identity-wallet/tests/custom-values/deployment_test.yaml @@ -82,7 +82,7 @@ tests: valueFrom: secretKeyRef: key: password - name: RELEASE-NAME-postgresql + name: verifiable-credential-revocation-service - name: APPLICATION_PORT value: "8080" - name: VC_EXPIRY_DATE diff --git a/charts/managed-identity-wallet/tests/default/deployment_test.yaml b/charts/managed-identity-wallet/tests/default/deployment_test.yaml index 1e2d3d0b..42b3df29 100644 --- a/charts/managed-identity-wallet/tests/default/deployment_test.yaml +++ b/charts/managed-identity-wallet/tests/default/deployment_test.yaml @@ -137,7 +137,7 @@ tests: valueFrom: secretKeyRef: key: password - name: RELEASE-NAME-postgresql + name: verifiable-credential-revocation-service - name: APPLICATION_PORT value: "8080" - name: VC_EXPIRY_DATE diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 327e51b1..09844c05 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -16,9 +16,8 @@ # # SPDX-License-Identifier: Apache-2.0 ############################################################### -# -# ----------------------------------------------- Values for Managed Identity Wallet ----------------------------------------------- # -# + +# -- Values for Managed Identity Wallet # -- The amount of replicas to run replicaCount: 1 # -- String to partially override common.names.fullname template (will maintain the release name) @@ -32,6 +31,7 @@ image: pullPolicy: Always # -- Image tag (empty one will use "appVersion" value from chart definition) tag: "" +imagePullSecrets: [] # -- Parameters for the application (will be stored as secrets - so, for passwords, ...) secrets: {} # -- envs Parameters for the application (will be provided as environment variables) @@ -48,8 +48,6 @@ service: type: ClusterIP # -- Kubernetes Service port port: 8080 -# -- Image pull secrets -imagePullSecrets: [] # -- Ingress Configuration ingress: # -- Enable ingress controller resource @@ -67,6 +65,7 @@ ingress: # - secretName: chart-example-tls # hosts: # - chart-example.local + className: nginx # -- Pod security configurations podSecurityContext: {} # -- Pod security parameters @@ -114,9 +113,7 @@ networkPolicy: # -- add volumes to the miw deployment extraVolumes: [] extraVolumeMounts: [] -# -# -----------------------------------------------MIW----------------------------------------------- # -# +# -- Values for MIW miw: ## @param miw.host Host name ## @param miw.logging.level Log level. Should be ether ERROR, WARN, INFO, DEBUG, or TRACE. @@ -150,7 +147,7 @@ miw: # -- Database name name: "miw_app" # -- Existing secret name for the database password - secret: "{{ .Release.Name }}-postgresql" + secret: "verifiable-credential-revocation-service" # -- Existing secret key for the database password secretPasswordKey: "password" # -- Password encryption configuratons @@ -194,32 +191,45 @@ miw: successThreshold: 1 # -- Number of seconds after which the probe times out. timeoutSeconds: 5 -# ----------------------------------------------- KEYCLOAK ----------------------------------------------- # -# For more information on how to configure the Keycloak chart see https://github.com/bitnami/charts/tree/main/bitnami/keycloak. + # For more information on how to configure the Keycloak chart see https://github.com/bitnami/charts/tree/main/bitnami/keycloak. +# -- Values for KEYCLOAK keycloak: # -- Enable to deploy Keycloak enabled: true # -- Extra environment variables extraEnvVars: [] - # - name: KEYCLOAK_HOSTNAME - # value: "{{ .Release.Name }}-keycloak" + # - name: KEYCLOAK_HOSTNAME + # value: "keycloak" postgresql: # -- Name of the PostgreSQL chart to deploy. Mandatory when the MIW deploys a PostgreSQL chart, too. nameOverride: "keycloak-postgresql" # -- Enable to deploy PostgreSQL enabled: true auth: - # -- Keycloak PostgreSQL user + # -- Postgresql admin user password username: "miw_keycloak" # -- KeycloakPostgresql password to set (if empty one is generated) - password: "" + password: "defaultpassword" # -- Database name database: "miw_keycloak" + volumePermissions: + enabled: true ingress: + # -- Enable ingress controller resource enabled: false + # -- Ingress annotations annotations: {} + # -- Ingress accepted hostnames hosts: [] + # - host: chart-example.local + # paths: + # - path: / + # pathType: Prefix + # -- Ingress TLS configuration tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local auth: # -- Keycloak admin user adminUser: "admin" @@ -232,20 +242,22 @@ keycloak: existingConfigmap: keycloak-realm-config # -- Number of retries before considering a Job as failed backoffLimit: 2 -# ----------------------------------------------- POSTGRESQL ----------------------------------------------- # +# -- Values for POSTGRESQL # For more information on how to configure the PostgreSQL chart see https://github.com/bitnami/charts/tree/main/bitnami/postgresql. postgresql: # -- Enable to deploy Postgresql enabled: true + image: + tag: "16-debian-12" + # -- Debug logs + debug: true auth: # -- Enable postgresql admin user enablePostgresUser: true - # -- Postgresql admin user password - postgresPassword: "adminpass" + # -- Postgresql root-user and non-root user secret + existingSecret: "verifiable-credential-revocation-service" # -- Postgresql user to create username: "miw" - # -- Postgresql password to set (if empty one is generated) - password: "adminpass" # -- Postgresql database to create database: "miw_app" # -- Creating a new database for VCRS application (Edit the DB configurations as required in configmap) @@ -259,18 +271,18 @@ postgresql: name: postgres-seed initdb: user: "postgres" - password: "adminpass" + password: "defaultpassword" scripts: init.sql: | CREATE DATABASE vcrs_app; - CREATE USER vcrs WITH ENCRYPTED PASSWORD 'adminpass'; + CREATE USER vcrs WITH ENCRYPTED PASSWORD 'defaultpassword'; GRANT ALL PRIVILEGES ON DATABASE vcrs_app TO vcrs; \c vcrs_app GRANT ALL ON SCHEMA public TO vcrs; backup: # -- Enable to create a backup cronjob enabled: false - #Cronjob Configuration + # -- Cronjob Configuration cronjob: # -- Backup schedule schedule: "* */6 * * *" @@ -284,9 +296,8 @@ postgresql: size: "8Gi" volumePermissions: enabled: true -# ----------------------------------------------- PGADMIN ----------------------------------------------- # +# -- Values for PGADMIN # For more information on how to configure the pgadmin chart see https://artifacthub.io/packages/helm/runix/pgadmin4. -# (Here we're using a stripped-down version of the pgadmin chart, to just ) pgadmin4: # -- Enable to deploy pgAdmin enabled: false @@ -328,9 +339,7 @@ pgadmin4: subPath: servers.json mountPath: "/pgadmin4/servers.json" readOnly: true -# -# ----------------------------------------------- Values for Verifiable Credential Revocation Service application ----------------------------------------------- # -# +# -- Values for Verifiable Credential Revocation Service application vcrs: replicaCount: 1 # -- Revocation application configuration @@ -339,12 +348,12 @@ vcrs: nameOverride: "verifiable-credential-revocation-service" # -- String to partially override common.names.fullname template (will maintain the release name) fullnameOverride: "verifiable-credential-revocation-service" - # -- ConfigMap Name - configName: "verifiable-credential-revocation-service-config" + # -- ConfigMap Name + configName: "verifiable-credential-revocation-service" # -- The Service name serviceName: "verifiable-credential-revocation-service" # -- The Secret name - secretName: "verifiable-credential-revocation-service-secret" + secretName: "verifiable-credential-revocation-service" image: # -- Image repository repository: public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service @@ -360,12 +369,12 @@ vcrs: # -- The application profile APPLICATION_PROFILE: local # -- The Database Host - DATABASE_HOST: "{{ .Release.Name }}-postgresql" + DATABASE_HOST: managed-identity-wallet-postgresql # -- The Database Port DATABASE_PORT: 5432 # -- The Database Name DATABASE_NAME: vcrs_app - # -- The Database SSL + # -- The Database SSL DATABASE_USE_SSL_COMMUNICATION: false # -- The Database Name DATABASE_USERNAME: vcrs @@ -381,7 +390,7 @@ vcrs: SERVICE_SECURITY_ENABLED: true # -- KeyClocak Configurations KEYCLOAK_REALM: miw_test - # -- ClientID Config + # -- ClientID Config KEYCLOAK_CLIENT_ID: miw_private_client # -- ClientID Config KEYCLOAK_PUBLIC_CLIENT_ID: miw_public_client @@ -395,7 +404,11 @@ vcrs: APP_LOG_LEVEL: INFO secrets: # -- The Database Password - DATABASE_PASSWORD: "adminpass" + DATABASE_PASSWORD: "defaultpassword" + # -- Postgresql password for MIW non-root User + password: "defaultpassword" + # -- Postgresql password for postgres root-user + postgres-password: "defaultpassword" resources: requests: # -- CPU resource requests @@ -434,21 +447,22 @@ vcrs: # -- Number of seconds after which the probe times out. timeoutSeconds: 15 # -- ingress configuration + ingressName: "verifiable-credential-revocation-service-ingress" ingress: enabled: false - className: "nginx" - annotations: - kubernetes.io/ingress.class: "nginx" - kubernetes.io/tls-acme: "true" + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" hosts: - - host: vcrs.example.org - paths: - - path: / - pathType: Prefix - tls: - - secretName: chart-example-tls - hosts: - - vcrs.example.org + # - host: chart-example.local + # paths: + # - path: / + # pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local service: # -- Kubernetes Service type type: ClusterIP From c162cada45cd0a885d0546214a18a32a7ac41f02 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Tue, 24 Sep 2024 15:03:42 +0530 Subject: [PATCH 52/60] fix: zap scan errors --- .github/workflows/release.yml | 250 ++++++++++++++++++ charts/managed-identity-wallet/README.md | 22 +- .../templates/networkpolicy.yaml | 2 +- .../templates/vcrs-deployment.yaml | 81 ++++-- .../templates/vcrs-hpa.yaml | 32 +++ charts/managed-identity-wallet/values.yaml | 58 ++-- 6 files changed, 389 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 charts/managed-identity-wallet/templates/vcrs-hpa.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e760bd4b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,250 @@ +# Copyright (c) 2021-2023 Contributors to the Eclipse Foundation + +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. + +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# SPDX-License-Identifier: Apache-2.0 +--- + + name: Semantic Release + on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + + env: + IMAGE_NAMESPACE: "tractusx" + IMAGE_NAME: "managed-identity-wallet" + + jobs: + + semantic_release: + name: Repository Release + runs-on: ubuntu-latest + permissions: + # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + contents: write + pull-requests: write + packages: write + outputs: + next_release: ${{ steps.semantic-release.outputs.next_release }} + will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + + - name: Setup Helm + uses: azure/setup-helm@v4.1.0 + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # setup helm-docs as it is needed during semantic-release + - uses: gabe565/setup-helm-docs-action@v1 + name: Setup helm-docs + if: github.event_name != 'pull_request' + with: + version: v1.11.3 + + - name: Run semantic release + id: semantic-release + if: github.event_name != 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release + + - name: Run semantic release (dry run) + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com + GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com + run: | + npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run + + - name: Execute Gradle build + run: ./gradlew build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: build + path: ./miw/build + if-no-files-found: error + retention-days: 1 + + - name: Upload Helm chart artifact + uses: actions/upload-artifact@v4 + with: + name: charts + path: ./charts + if-no-files-found: error + retention-days: 1 + + - name: Report semantic-release outputs + run: | + echo "::notice::${{ env.next_release }}" + echo "::notice::${{ env.will_create_new_release }}" + + - name: Upload jar to GitHub release + if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} + run: | + echo "::notice::Uploading jar to GitHub release" + gh release upload "v$RELEASE_VERSION" ./miw/build/libs/miw-latest.jar + + docker: + name: Docker Release + needs: semantic_release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build + path: ./miw/build + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + # Create SemVer or ref tags dependent of trigger event + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + # Automatically prepare image tags; See action docs for more examples. + # semver patter will generate tags like these for example :1 :1.2 :1.2.3 + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} + type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: DockerHub login + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + # Use existing DockerHub credentials present as secrets + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + - name: Push image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + # https://github.com/peter-evans/dockerhub-description + # Important step to push image description to DockerHub + - name: Update Docker Hub description + if: github.event_name != 'pull_request' + uses: peter-evans/dockerhub-description@v3 + with: + # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' + readme-filepath: Docker-hub-notice.md + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} + + helm: + name: Helm Release + needs: semantic_release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download Helm chart artifact + uses: actions/download-artifact@v4 + with: + name: charts + path: ./charts + + - name: Install Helm + uses: azure/setup-helm@v4.1.0 + + - name: Add Helm dependency repositories + run: | + helm repo add bitnami https://charts.bitnami.com/bitnami + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Release chart + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + run: | + # Package MIW chart + helm_package_path=$(helm package -u -d helm-charts ./charts/managed-identity-wallet | grep -o 'to: .*' | cut -d' ' -f2-) + echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV + + # Commit and push to gh-pages + git add helm-charts + git stash -- helm-charts + git reset --hard + git fetch origin + git checkout gh-pages + git stash pop + + # Generate helm repo index.yaml + helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ + git add index.yaml + + git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" + + git push origin gh-pages + + - name: Upload chart to GitHub release + if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} + HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} + run: | + echo "::notice::Uploading chart to GitHub release" + gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" \ No newline at end of file diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index c157e1e5..8aff8490 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -201,7 +201,7 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | | serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | | tolerations | list | `[]` | Tolerations configuration | -| vcrs | object | `{"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service","tag":"latest"},"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":5,"initialDelaySeconds":60,"periodSeconds":15,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","readinessProbe":{"enabled":true,"failureThreshold":5,"initialDelaySeconds":60,"periodSeconds":15,"successThreshold":1,"timeoutSeconds":15},"replicaCount":1,"resources":{"limits":{"cpu":"500m","memory":"1Gi"},"requests":{"cpu":"250m","memory":"512Mi"}},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"serviceName":"verifiable-credential-revocation-service"}` | Values for Verifiable Credential Revocation Service application | +| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | | vcrs.configName | string | `"verifiable-credential-revocation-service"` | ConfigMap Name | | vcrs.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | | vcrs.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | @@ -230,23 +230,19 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | vcrs.image.tag | string | `"latest"` | Image tag (empty one will use "appVersion" value from chart definition) | | vcrs.ingress.service.port | int | `8081` | Kubernetes Service port | | vcrs.ingress.service.type | string | `"ClusterIP"` | Kubernetes Service type | -| vcrs.livenessProbe | object | `{"enabled":true,"failureThreshold":5,"initialDelaySeconds":60,"periodSeconds":15,"timeoutSeconds":30}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | +| vcrs.livenessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30}` | Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | | vcrs.livenessProbe.enabled | bool | `true` | Enables/Disables the livenessProbe at all | -| vcrs.livenessProbe.failureThreshold | int | `5` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | -| vcrs.livenessProbe.initialDelaySeconds | int | `60` | Number of seconds after the container has started before readiness probes are initiated. | -| vcrs.livenessProbe.periodSeconds | int | `15` | How often (in seconds) to perform the probe | +| vcrs.livenessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. | +| vcrs.livenessProbe.initialDelaySeconds | int | `60` | Number of seconds after the container has started before readiness probe are initiated. | +| vcrs.livenessProbe.periodSeconds | int | `5` | How often (in seconds) to perform the probe | | vcrs.livenessProbe.timeoutSeconds | int | `30` | Number of seconds after which the probe times out. | | vcrs.nameOverride | string | `"verifiable-credential-revocation-service"` | The configmap name | +| vcrs.readinessProbe | object | `{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30}` | Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) | | vcrs.readinessProbe.enabled | bool | `true` | Enables/Disables the readinessProbe at all | -| vcrs.readinessProbe.failureThreshold | int | `5` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | +| vcrs.readinessProbe.failureThreshold | int | `3` | When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. | | vcrs.readinessProbe.initialDelaySeconds | int | `60` | Number of seconds after the container has started before readiness probe are initiated. | -| vcrs.readinessProbe.periodSeconds | int | `15` | How often (in seconds) to perform the probe | -| vcrs.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes for the probe to be considered successful after having failed. | -| vcrs.readinessProbe.timeoutSeconds | int | `15` | Number of seconds after which the probe times out. | -| vcrs.resources.limits.cpu | string | `"500m"` | CPU resource limits | -| vcrs.resources.limits.memory | string | `"1Gi"` | Memory resource limits | -| vcrs.resources.requests.cpu | string | `"250m"` | CPU resource requests | -| vcrs.resources.requests.memory | string | `"512Mi"` | Memory resource requests | +| vcrs.readinessProbe.periodSeconds | int | `30` | How often (in seconds) to perform the probe | +| vcrs.readinessProbe.timeoutSeconds | int | `30` | Number of seconds after which the probe times out. | | vcrs.secretName | string | `"verifiable-credential-revocation-service"` | The Secret name | | vcrs.secrets.DATABASE_PASSWORD | string | `"defaultpassword"` | The Database Password | | vcrs.secrets.password | string | `"defaultpassword"` | Postgresql password for MIW non-root User | diff --git a/charts/managed-identity-wallet/templates/networkpolicy.yaml b/charts/managed-identity-wallet/templates/networkpolicy.yaml index 425016e6..2edaefbc 100644 --- a/charts/managed-identity-wallet/templates/networkpolicy.yaml +++ b/charts/managed-identity-wallet/templates/networkpolicy.yaml @@ -1,5 +1,5 @@ # /******************************************************************************** -# * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation +# * Copyright (c) 2024 Contributors to the Eclipse Foundation # * # * See the NOTICE file(s) distributed with this work for additional # * information regarding copyright ownership. diff --git a/charts/managed-identity-wallet/templates/vcrs-deployment.yaml b/charts/managed-identity-wallet/templates/vcrs-deployment.yaml index 95db61ce..16179582 100644 --- a/charts/managed-identity-wallet/templates/vcrs-deployment.yaml +++ b/charts/managed-identity-wallet/templates/vcrs-deployment.yaml @@ -24,38 +24,49 @@ metadata: labels: {{- include "verifiable-credential-revocation-service.labels" . | nindent 4 }} spec: - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 0 - maxSurge: 1 + {{- if not .Values.vcrs.autoscaling.enabled }} + replicas: {{ .Values.vcrs.replicaCount }} + {{- end }} selector: matchLabels: {{- include "verifiable-credential-revocation-service.selectorLabels" . | nindent 6 }} - replicas: {{ .Values.vcrs.replicaCount }} - revisionHistoryLimit: 2 + strategy: + {{- if .Values.vcrs.rollingUpdate.enabled }} + type: RollingUpdate + rollingUpdate: + maxSurge: {{ .Values.vcrs.rollingUpdate.rollingUpdateMaxSurge }} + maxUnavailable: {{ .Values.vcrs.rollingUpdate.rollingUpdateMaxUnavailable }} + {{- end }} template: metadata: + {{- with .Values.vcrs.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} labels: - {{- include "verifiable-credential-revocation-service.selectorLabels" . | nindent 8 }} + {{- include "verifiable-credential-revocation-service.labels" . | nindent 8 }} + {{- with .Values.vcrs.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} spec: + {{- with .Values.vcrs.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.vcrs.podSecurityContext | nindent 8 }} containers: - - name: {{ include "verifiable-credential-revocation-service.fullname" . }} - image: {{ .Values.vcrs.image.repository }}:{{ default .Chart.AppVersion .Values.vcrs.image.tag }} + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.vcrs.securityContext | nindent 12 }} + image: "{{ .Values.vcrs.image.repository }}:{{ .Values.vcrs.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.vcrs.image.pullPolicy }} - resources: - {{- toYaml .Values.vcrs.resources | nindent 12 }} - envFrom: - - secretRef: - name: {{ .Values.vcrs.secretName }} - - configMapRef: - name: {{ .Values.vcrs.configName }} - {{- with .Values.vcrs.livenessProbe }} - {{- if .enabled }} ports: - name: http - containerPort: 8081 + containerPort: {{ .Values.vcrs.ingress.service.port }} protocol: TCP + {{- with .Values.vcrs.livenessProbe }} + {{- if .enabled }} livenessProbe: httpGet: path: /actuator/health/liveness @@ -77,7 +88,33 @@ spec: failureThreshold: {{ .failureThreshold }} initialDelaySeconds: {{ .initialDelaySeconds }} periodSeconds: {{ .periodSeconds }} - successThreshold: {{ .successThreshold }} timeoutSeconds: {{ .timeoutSeconds }} {{- end }} - {{- end }} \ No newline at end of file + {{- end }} + resources: + {{- toYaml .Values.vcrs.resources | nindent 12 }} + envFrom: + - secretRef: + name: {{ .Values.vcrs.secretName }} + - configMapRef: + name: {{ .Values.vcrs.configName }} + {{- with .Values.vcrs.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.vcrs.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.vcrs.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.vcrs.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.vcrs.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/charts/managed-identity-wallet/templates/vcrs-hpa.yaml b/charts/managed-identity-wallet/templates/vcrs-hpa.yaml new file mode 100644 index 00000000..9c5ae5a8 --- /dev/null +++ b/charts/managed-identity-wallet/templates/vcrs-hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.vcrs.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "verifiable-credential-revocation-service.fullname" . }} + labels: + {{- include "verifiable-credential-revocation-service.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "verifiable-credential-revocation-service.fullname" . }} + minReplicas: {{ .Values.vcrs.autoscaling.minReplicas }} + maxReplicas: {{ .Values.vcrs.autoscaling.maxReplicas }} + metrics: + {{- if .Values.vcrs.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.vcrs.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.vcrs.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.vcrs.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 09844c05..4a631b78 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -409,44 +409,53 @@ vcrs: password: "defaultpassword" # -- Postgresql password for postgres root-user postgres-password: "defaultpassword" - resources: - requests: - # -- CPU resource requests - cpu: 250m - # -- Memory resource requests - memory: 512Mi - limits: - # -- CPU resource limits - cpu: 500m - # -- Memory resource limits - memory: 1Gi + podAnnotations: {} + podLabels: {} + imagePullSecrets: [] + rollingUpdate: + enabled: true + # Minimum number of pods that should be running during the update process. + rollingUpdateMaxSurge: 1 + # Maximum number of pods that can be unavailable during the update process. + rollingUpdateMaxUnavailable: 0 + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi # -- Kubernetes [liveness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) livenessProbe: # -- Enables/Disables the livenessProbe at all enabled: true # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. Giving up in case of liveness probe means restarting the container. - failureThreshold: 5 - # -- Number of seconds after the container has started before readiness probes are initiated. + failureThreshold: 3 + # -- Number of seconds after the container has started before readiness probe are initiated. initialDelaySeconds: 60 # -- Number of seconds after which the probe times out. timeoutSeconds: 30 # -- How often (in seconds) to perform the probe - periodSeconds: 15 - # -- Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + periodSeconds: 5 + # -- Kubernetes [readiness-probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) readinessProbe: # -- Enables/Disables the readinessProbe at all enabled: true # -- When a probe fails, Kubernetes will try failureThreshold times before giving up. In case of readiness probe the Pod will be marked Unready. - failureThreshold: 5 + failureThreshold: 3 # -- Number of seconds after the container has started before readiness probe are initiated. initialDelaySeconds: 60 # -- How often (in seconds) to perform the probe - periodSeconds: 15 - # -- Minimum consecutive successes for the probe to be considered successful after having failed. - successThreshold: 1 + periodSeconds: 30 # -- Number of seconds after which the probe times out. - timeoutSeconds: 15 + timeoutSeconds: 30 # -- ingress configuration + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 ingressName: "verifiable-credential-revocation-service-ingress" ingress: enabled: false @@ -476,3 +485,12 @@ vcrs: secret: "" # -- Existing secret key for database encryption key secretKey: "" + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + volumes: [] + # Additional volumeMounts on the output Deployment definition. + volumeMounts: [] + nodeSelector: {} + tolerations: [] + affinity: {} From 074ab2df61f94776d23b45eebc87220fbd61a0ef Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Tue, 24 Sep 2024 15:06:27 +0530 Subject: [PATCH 53/60] fix: zap scan errors --- charts/managed-identity-wallet/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 4a631b78..7bbd6931 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -356,7 +356,7 @@ vcrs: secretName: "verifiable-credential-revocation-service" image: # -- Image repository - repository: public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service + repository: tractusx/verifiable-credential-revocation-service # -- PullPolicy pullPolicy: IfNotPresent # -- Image tag (empty one will use "appVersion" value from chart definition) From e1218df658eda6195caf44d5567a09045da62813 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Tue, 24 Sep 2024 15:22:01 +0530 Subject: [PATCH 54/60] docs: fixed readme file --- charts/managed-identity-wallet/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 8aff8490..1fcd799f 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -201,7 +201,7 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | | serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | | tolerations | list | `[]` | Tolerations configuration | -| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | +| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | | vcrs.configName | string | `"verifiable-credential-revocation-service"` | ConfigMap Name | | vcrs.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | | vcrs.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | @@ -226,7 +226,7 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | vcrs.fullnameOverride | string | `"verifiable-credential-revocation-service"` | String to partially override common.names.fullname template (will maintain the release name) | | vcrs.host | string | `"localhost"` | Revocation application configuration | | vcrs.image.pullPolicy | string | `"IfNotPresent"` | PullPolicy | -| vcrs.image.repository | string | `"public.ecr.aws/w6s7t8e0/tractusx/verifiable-credential-revocation-service"` | Image repository | +| vcrs.image.repository | string | `"tractusx/verifiable-credential-revocation-service"` | Image repository | | vcrs.image.tag | string | `"latest"` | Image tag (empty one will use "appVersion" value from chart definition) | | vcrs.ingress.service.port | int | `8081` | Kubernetes Service port | | vcrs.ingress.service.type | string | `"ClusterIP"` | Kubernetes Service type | From 14a67e1b982569ddf6e3becd8176fc346b5ef731 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Tue, 24 Sep 2024 15:25:56 +0530 Subject: [PATCH 55/60] fix: modefied the value for the replicas --- charts/managed-identity-wallet/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 7bbd6931..96e70c47 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -453,7 +453,7 @@ vcrs: autoscaling: enabled: false minReplicas: 1 - maxReplicas: 100 + maxReplicas: 2 targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: 80 ingressName: "verifiable-credential-revocation-service-ingress" From 571d994ddc62c3a4461d2da2d07213fa40749485 Mon Sep 17 00:00:00 2001 From: rohit-smartsensesolutions Date: Tue, 24 Sep 2024 15:27:36 +0530 Subject: [PATCH 56/60] fixed helm docs --- charts/managed-identity-wallet/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 1fcd799f..537db4c9 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -201,7 +201,7 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | | serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | | tolerations | list | `[]` | Tolerations configuration | -| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":100,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | +| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":2,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | | vcrs.configName | string | `"verifiable-credential-revocation-service"` | ConfigMap Name | | vcrs.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | | vcrs.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | From 00a86d50be726936dfe2857d23142021dfe87509 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Wed, 25 Sep 2024 17:44:27 +0530 Subject: [PATCH 57/60] docs: README and sample in API doc are updated --- .github/workflows/release.yml | 250 ------------------ charts/managed-identity-wallet/README.md | 2 +- charts/managed-identity-wallet/values.yaml | 2 +- docs/api/revocation-service/openapi_v001.json | 14 +- docs/arc42/revocation-service/main.md | 30 +-- .../apidocs/RevocationAPIDoc.java | 3 +- revocation-service/README.md | 15 +- .../RevocationApiControllerApiDocs.java | 14 +- .../revocation/TestUtil.java | 4 +- 9 files changed, 42 insertions(+), 292 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e760bd4b..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright (c) 2021-2023 Contributors to the Eclipse Foundation - -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. - -# This program and the accompanying materials are made available under the -# terms of the Apache License, Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0. - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# SPDX-License-Identifier: Apache-2.0 ---- - - name: Semantic Release - on: - push: - branches: - - main - - develop - pull_request: - branches: - - main - - develop - - env: - IMAGE_NAMESPACE: "tractusx" - IMAGE_NAME: "managed-identity-wallet" - - jobs: - - semantic_release: - name: Repository Release - runs-on: ubuntu-latest - permissions: - # see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs - contents: write - pull-requests: write - packages: write - outputs: - next_release: ${{ steps.semantic-release.outputs.next_release }} - will_create_new_release: ${{ steps.semantic-release.outputs.will_create_new_release }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v2 - - - name: Setup Helm - uses: azure/setup-helm@v4.1.0 - - - name: Setup JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - # setup helm-docs as it is needed during semantic-release - - uses: gabe565/setup-helm-docs-action@v1 - name: Setup helm-docs - if: github.event_name != 'pull_request' - with: - version: v1.11.3 - - - name: Run semantic release - id: semantic-release - if: github.event_name != 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com - GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com - run: | - npx --yes -p @semantic-release/exec -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release - - - name: Run semantic release (dry run) - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_EMAIL: ${{ github.actor }}@users.noreply.github.com - GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com - run: | - npx --yes -p @semantic-release/exec -p @semantic-release/github -p @semantic-release/changelog -p @semantic-release/git -p @semantic-release/commit-analyzer -p @semantic-release/release-notes-generator semantic-release --dry-run - - - name: Execute Gradle build - run: ./gradlew build - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: build - path: ./miw/build - if-no-files-found: error - retention-days: 1 - - - name: Upload Helm chart artifact - uses: actions/upload-artifact@v4 - with: - name: charts - path: ./charts - if-no-files-found: error - retention-days: 1 - - - name: Report semantic-release outputs - run: | - echo "::notice::${{ env.next_release }}" - echo "::notice::${{ env.will_create_new_release }}" - - - name: Upload jar to GitHub release - if: github.event_name != 'pull_request' && steps.semantic-release.outputs.will_create_new_release == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ steps.semantic-release.outputs.next_release }} - run: | - echo "::notice::Uploading jar to GitHub release" - gh release upload "v$RELEASE_VERSION" ./miw/build/libs/miw-latest.jar - - docker: - name: Docker Release - needs: semantic_release - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: build - path: ./miw/build - - - name: Download Helm chart artifact - uses: actions/download-artifact@v4 - with: - name: charts - path: ./charts - - # Create SemVer or ref tags dependent of trigger event - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} - # Automatically prepare image tags; See action docs for more examples. - # semver patter will generate tags like these for example :1 :1.2 :1.2.3 - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}},value=${{ needs.semantic_release.outputs.next_release }} - type=semver,pattern={{major}},value=${{ needs.semantic_release.outputs.next_release }} - type=semver,pattern={{major}}.{{minor}},value=${{ needs.semantic_release.outputs.next_release }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} - - - name: DockerHub login - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - # Use existing DockerHub credentials present as secrets - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Push image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - # https://github.com/peter-evans/dockerhub-description - # Important step to push image description to DockerHub - - name: Update Docker Hub description - if: github.event_name != 'pull_request' - uses: peter-evans/dockerhub-description@v3 - with: - # readme-filepath defaults to toplevel README.md, Only necessary if you have a dedicated file with your 'Notice for docker images' - readme-filepath: Docker-hub-notice.md - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - repository: ${{ env.IMAGE_NAMESPACE }}/${{ env.IMAGE_NAME }} - - helm: - name: Helm Release - needs: semantic_release - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download Helm chart artifact - uses: actions/download-artifact@v4 - with: - name: charts - path: ./charts - - - name: Install Helm - uses: azure/setup-helm@v4.1.0 - - - name: Add Helm dependency repositories - run: | - helm repo add bitnami https://charts.bitnami.com/bitnami - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - - - name: Release chart - if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' - run: | - # Package MIW chart - helm_package_path=$(helm package -u -d helm-charts ./charts/managed-identity-wallet | grep -o 'to: .*' | cut -d' ' -f2-) - echo "HELM_PACKAGE_PATH=$helm_package_path" >> $GITHUB_ENV - - # Commit and push to gh-pages - git add helm-charts - git stash -- helm-charts - git reset --hard - git fetch origin - git checkout gh-pages - git stash pop - - # Generate helm repo index.yaml - helm repo index . --merge index.yaml --url https://${GITHUB_REPOSITORY_OWNER}.github.io/${GITHUB_REPOSITORY#*/}/ - git add index.yaml - - git commit -s -m "Release ${{ needs.semantic_release.outputs.next_release }}" - - git push origin gh-pages - - - name: Upload chart to GitHub release - if: github.event_name != 'pull_request' && needs.semantic_release.outputs.will_create_new_release == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_VERSION: ${{ needs.semantic_release.outputs.next_release }} - HELM_PACKAGE_PATH: ${{ env.HELM_PACKAGE_PATH }} - run: | - echo "::notice::Uploading chart to GitHub release" - gh release upload "v$RELEASE_VERSION" "$HELM_PACKAGE_PATH" \ No newline at end of file diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 537db4c9..9c524424 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -201,7 +201,7 @@ See [helm upgrade](https://helm.sh/docs/helm/helm_upgrade/) for command document | serviceAccount.create | bool | `true` | Enable creation of ServiceAccount | | serviceAccount.name | string | `""` | The name of the ServiceAccount to use. | | tolerations | list | `[]` | Tolerations configuration | -| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":2,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | +| vcrs | object | `{"affinity":{},"autoscaling":{"enabled":false,"maxReplicas":2,"minReplicas":1,"targetCPUUtilizationPercentage":80,"targetMemoryUtilizationPercentage":80},"configName":"verifiable-credential-revocation-service","database":{"encryptionKey":{"secret":"","secretKey":"","value":""}},"env":{"APPLICATION_LOG_LEVEL":"DEBUG","APPLICATION_NAME":"verifiable-credential-revocation-service","APPLICATION_PORT":8081,"APPLICATION_PROFILE":"local","APP_LOG_LEVEL":"INFO","AUTH_SERVER_URL":"http://{{ .Release.Name }}-keycloak","DATABASE_CONNECTION_POOL_SIZE":10,"DATABASE_HOST":"managed-identity-wallet-postgresql","DATABASE_NAME":"vcrs_app","DATABASE_PORT":5432,"DATABASE_USERNAME":"vcrs","DATABASE_USE_SSL_COMMUNICATION":false,"DOMAIN_URL":"https://977d-203-129-213-107.ngrok-free.app","ENABLE_API_DOC":true,"ENABLE_SWAGGER_UI":true,"KEYCLOAK_CLIENT_ID":"miw_private_client","KEYCLOAK_PUBLIC_CLIENT_ID":"miw_public_client","KEYCLOAK_REALM":"miw_test","MIW_URL":"https://a888-203-129-213-107.ngrok-free.app","SERVICE_SECURITY_ENABLED":true,"VC_SCHEMA_LINK":"https://www.w3.org/2018/credentials/v1, https://w3id.org/vc/status-list/2021/v1"},"fullnameOverride":"verifiable-credential-revocation-service","host":"localhost","image":{"pullPolicy":"IfNotPresent","repository":"tractusx/verifiable-credential-revocation-service","tag":"latest"},"imagePullSecrets":[],"ingress":{"annotations":{},"className":"","enabled":false,"hosts":null,"service":{"port":8081,"type":"ClusterIP"},"tls":[]},"ingressName":"verifiable-credential-revocation-service-ingress","livenessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":5,"timeoutSeconds":30},"nameOverride":"verifiable-credential-revocation-service","nodeSelector":{},"podAnnotations":{},"podLabels":{},"podSecurityContext":{},"readinessProbe":{"enabled":true,"failureThreshold":3,"initialDelaySeconds":60,"periodSeconds":30,"timeoutSeconds":30},"replicaCount":1,"resources":{},"rollingUpdate":{"enabled":true,"rollingUpdateMaxSurge":1,"rollingUpdateMaxUnavailable":0},"secretName":"verifiable-credential-revocation-service","secrets":{"DATABASE_PASSWORD":"defaultpassword","password":"defaultpassword","postgres-password":"defaultpassword"},"securityContext":{"allowPrivilegeEscalation":false},"serviceName":"verifiable-credential-revocation-service","tolerations":[],"volumeMounts":[],"volumes":[]}` | Values for Verifiable Credential Revocation Service application | | vcrs.configName | string | `"verifiable-credential-revocation-service"` | ConfigMap Name | | vcrs.database.encryptionKey.secret | string | `""` | Existing secret for database encryption key | | vcrs.database.encryptionKey.secretKey | string | `""` | Existing secret key for database encryption key | diff --git a/charts/managed-identity-wallet/values.yaml b/charts/managed-identity-wallet/values.yaml index 96e70c47..16849bd3 100644 --- a/charts/managed-identity-wallet/values.yaml +++ b/charts/managed-identity-wallet/values.yaml @@ -398,7 +398,7 @@ vcrs: AUTH_SERVER_URL: "http://{{ .Release.Name }}-keycloak" # -- Revocation application configuration MIW_URL: https://a888-203-129-213-107.ngrok-free.app - VC_SCHEMA_LINK: https://www.w3.org/2018/credentials/v1, https://cofinity-x.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json + VC_SCHEMA_LINK: https://www.w3.org/2018/credentials/v1, https://w3id.org/vc/status-list/2021/v1 DOMAIN_URL: https://977d-203-129-213-107.ngrok-free.app # Application logging configurations APP_LOG_LEVEL: INFO diff --git a/docs/api/revocation-service/openapi_v001.json b/docs/api/revocation-service/openapi_v001.json index 423ead47..0e95ff5a 100644 --- a/docs/api/revocation-service/openapi_v001.json +++ b/docs/api/revocation-service/openapi_v001.json @@ -47,7 +47,7 @@ "statusPurpose" : "revocation", "statusListIndex" : "12", "statusListCredential" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type" : "BitstringStatusListEntry" + "type" : "StatusList2021" } } }, @@ -122,7 +122,7 @@ "statusPurpose" : "revocation", "statusListIndex" : "17", "statusListCredential" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type" : "BitstringStatusListEntry" + "type" : "StatusList2021" } } } @@ -155,7 +155,7 @@ "statusPurpose" : "revocation", "statusListIndex" : "12", "statusListCredential" : "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type" : "BitstringStatusListEntry" + "type" : "StatusList2021" } } }, @@ -170,7 +170,7 @@ "content" : { "application/json" : { "example" : { - "type" : "BitstringStatusListEntry", + "type" : "StatusList2021", "title" : "Revocation service error", "status" : "409", "detail" : "Credential already revoked", @@ -240,13 +240,13 @@ "example" : { "@context" : [ "https://www.w3.org/2018/credentials/v1", - "https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json", + "https://w3id.org/vc/status-list/2021/v1", "https://w3id.org/security/suites/jws-2020/v1" ], "id" : "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", "type" : [ "VerifiableCredential", - "BitstringStatusListCredential" + "StatusList2021Credential" ], "issuer" : "did:web:localhost:BPNL000000000000", "issuanceDate" : "2024-02-05T09:39:58Z", @@ -254,7 +254,7 @@ { "statusPurpose" : "revocation", "id" : "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type" : "BitstringStatusList", + "type" : "StatusList2021", "encodedList" : "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" } ], diff --git a/docs/arc42/revocation-service/main.md b/docs/arc42/revocation-service/main.md index 3c334cf3..8afdad6e 100644 --- a/docs/arc42/revocation-service/main.md +++ b/docs/arc42/revocation-service/main.md @@ -13,7 +13,7 @@ not work. Simply deleting credentials will not work as there might be possible that holder save credentials in other location and present it to verifier. -When any business partner deboarded from Cofinity-X +When any business partner deboarded from Catena-X When there a any changes in credentials or updates needed in credentials. In this case, we need to revoke the older VC and need to reissue it. @@ -26,17 +26,17 @@ The core functionalities are: ## Cross-cutting Concepts -Please refer to this for more information: [Bitstring Status List v1.0](https://www.w3.org/TR/vc-bitstring-status-list/) +Please refer to this for more information: [Status List 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427) ## Requirements Overview The basic requirements for the Managed Identity Wallet are as follows: -- issue status list to all issuer using REST API +- issue status list to all issuers using REST API - Manage status list index for each issuer -- Allow issuer to revoke credential using REST API +- Allow issuers to revoke credentials using REST API - Allow verifier to verify status of credential using REST API @@ -48,7 +48,7 @@ The basic requirements for the Managed Identity Wallet are as follows: 2. The current index should be created for each issued revocable VC 3. while revocation, the correct index should be revoked 4. The application should work in case of horizontal scanning -5. Only Authorizae user/client can access the revocation API +5. Only Authorize user/client can access the revocation API 6. One status list index should be created for one VC 7. Sonar quality gate should be passed 8. No issues in veracode scanning @@ -58,7 +58,7 @@ The basic requirements for the Managed Identity Wallet are as follows: The key stakeholders of the component are: -- Issuer: Issuer should able to issue revocable credentials and able to revoke issued credentials when there a need +- Issuer: Issuer should be able to issue revocable credentials and able to revoke issued credentials when there a need - Verifier: Verify status of credential(active/revoked) along with signature and expiry date verification @@ -139,7 +139,7 @@ Response Body: "statusPurpose": "revocation", "statusListIndex": "12", "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } ``` @@ -159,13 +159,13 @@ Response: { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json", + "https://w3id.org/vc/status-list/2021/v1", "https://w3id.org/security/suites/jws-2020/v1" ], "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", "type": [ "VerifiableCredential", - "BitstringStatusListCredential" + "StatusList2021Credential" ], "issuer": "did:web:localhost:BPNL000000000000", "issuanceDate": "2024-02-05T09:39:58Z", @@ -173,7 +173,7 @@ Response: { "statusPurpose": "revocation", "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusList", + "type": "StatusList2021", "encodedList": "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" } ], @@ -203,7 +203,7 @@ Request: "statusPurpose": "revocation", "statusListIndex": "12", "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } ``` @@ -227,7 +227,7 @@ Request: "statusPurpose": "revocation", "statusListIndex": "12", "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } ``` @@ -333,11 +333,11 @@ For local setup, instruction will be added in README.md file # Guiding Concepts -Please refer: https://www.w3.org/TR/vc-bitstring-status-list/ +Please refer: https://www.w3.org/TR/2023/WD-vc-status-list-20230427 # Design Decisions -Revocation service is developed at Cofinity-X and as per discussion with product owner of MIW cofinity-x has decided to +Revocation service is developed at Cofinity-X and as per discussion with a product owner of MIW cofinity-x has decided to contribute to the eclipse tractus-x # Quality Requirements @@ -389,7 +389,7 @@ requirements where relevant and applicable: | VC | Verifiable Credential | | VP | Verifiable Presentation | | Wallet | Virtual placeholder for business partner which holds VCs | -| Base wallet | Wallet for Cofinity-X. CX type of VC will be issued using this wallet | +| Base wallet | Wallet for Operating company . CX type of VC will be issued using this wallet | | Status list credential | [https://www.w3.org/TR/vc-status-list/#statuslist2021credential](https://www.w3.org/TR/vc-status-list/#statuslist2021credential) | | Status list entry | [https://www.w3.org/TR/vc-status-list/#statuslist2021credential](https://www.w3.org/TR/vc-status-list/#statuslist2021credential) | | Status list index | [https://www.w3.org/TR/vc-status-list/#statuslist2021entry](https://www.w3.org/TR/vc-status-list/#statuslist2021entry) | diff --git a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java index ed03dbaf..d631047f 100644 --- a/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java +++ b/miw/src/main/java/org/eclipse/tractusx/managedidentitywallets/apidocs/RevocationAPIDoc.java @@ -134,7 +134,6 @@ public class RevocationAPIDoc { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://cofinity-x.github.io/schema-registry/v1.1/SummaryVC.json", "https://w3id.org/security/suites/jws-2020/v1", "https://w3id.org/vc/status-list/2021/v1" ], @@ -146,7 +145,7 @@ public class RevocationAPIDoc { "statusPurpose": "revocation", "statusListIndex": "1", "statusListCredential": "https://7337-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials?issuerId=did:web:localhost:BPNL000000000000", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } } """)) diff --git a/revocation-service/README.md b/revocation-service/README.md index 2f27eec6..9130d099 100644 --- a/revocation-service/README.md +++ b/revocation-service/README.md @@ -1,6 +1,7 @@ -# Bitstring Statuslist Service +# Statuslist2021 revocation service Service -This service is responsible for managing the status of credentials using a Bitstring status list. It supports operations such as creating, revoking, and retrieving credential statuses. +This service is responsible for managing the status of credentials using a status list 2021. +It supports operations such as creating, revoking, and retrieving credential statuses. ## Prerequisites @@ -11,7 +12,7 @@ Before you begin, ensure you have met the following requirements: - Keycloak service is operational and accessible. - Postgres database service is running and accessible. - Environment variables are configured according to the application's requirements. -- MIW is deployed and accessable +- MIW is deployed and accessible - Be sure the right ssi-lib version is installed ## Environment Configuration @@ -47,7 +48,7 @@ The application can be configured using environment variables. Below are the ava The application integrates with Keycloak for OAuth2 authentication and authorization: -- **SERVICE_SECURITY_ENABLED**: Flag to enable or disable Servive Security integration for Disabling Swagger and other Endpoints. Defaults to true, false only for test purposes recommended. +- **SERVICE_SECURITY_ENABLED**: Flag to enable or disable service Security integration for Disabling Swagger and other Endpoints. Defaults to true, false only for test purposes recommended. The application integrates with Keycloak for OAuth2 authentication and authorization: @@ -80,15 +81,15 @@ Be sure to replace placeholder values in the environment variables with actual d Ensure that the middleware (MIW) is running, as it is used to sign the status list credentials. -An Overview how to start the middleware can be found under the Readme.md in here:[README.md](..%2Fmiw%2FREADME.md) +An Overview of how to start the middleware can be found under the Readme.md in here:[README.md](..%2Fmiw%2FREADME.md) ## Starting Services -To start the Bitstring Statuslist Service, follow these steps: +To start the Statuslist2021 Service, follow these steps: 1. **Start Keycloak and Postgres:** - Ensure that both Keycloak and Postgres services are running. For development purposes the Keycloak and + Ensure that both Keycloak and Postgres services are running. For development purposes, the Keycloak and Postgres from the MIW Dev Setup can be used if not already running with the MIW Task deployment. diff --git a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java index 0f63fc68..c93c0794 100644 --- a/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java +++ b/revocation-service/src/main/java/org/eclipse/tractusx/managedidentitywallets/revocation/apidocs/RevocationApiControllerApiDocs.java @@ -81,7 +81,7 @@ public class RevocationApiControllerApiDocs { "statusPurpose": "revocation", "statusListIndex": "12", "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } """), mediaType = "application/json") @@ -117,7 +117,7 @@ public class RevocationApiControllerApiDocs { @ExampleObject( value = """ { - "type": "BitstringStatusListEntry", + "type": "StatusList2021", "title": "Revocation service error", "status": "409", "detail": "Credential already revoked", @@ -142,7 +142,7 @@ public class RevocationApiControllerApiDocs { "statusPurpose": "revocation", "statusListIndex": "12", "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } """), mediaType = "application/json") @@ -170,7 +170,7 @@ public class RevocationApiControllerApiDocs { "statusPurpose": "revocation", "statusListIndex": "17", "statusListCredential": "https://977d-203-129-213-107.ngrok-free.app/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusListEntry" + "type": "StatusList2021" } """), mediaType = "application/json") @@ -223,14 +223,14 @@ public class RevocationApiControllerApiDocs { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://eclipse-tractusx.github.io/schema-registry/w3c/v1.0/BitstringStatusList.json", + "https://w3id.org/vc/status-list/2021/v1", "https://w3id.org/security/suites/jws-2020/v1" ], "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", "type": [ "VerifiableCredential", - "BitstringStatusListCredential" + "StatusList2021Credential" ], "issuer": "did:web:localhost:BPNL000000000000", "issuanceDate": "2024-02-05T09:39:58Z", @@ -239,7 +239,7 @@ public class RevocationApiControllerApiDocs { { "statusPurpose": "revocation", "id": "http://localhost/api/v1/revocations/credentials/BPNL000000000000/revocation/1", - "type": "BitstringStatusList", + "type": "StatusList2021", "encodedList": "H4sIAAAAAAAA/wMAAAAAAAAAAAA=" } ], diff --git a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java index 6dc27476..69df4ade 100644 --- a/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java +++ b/revocation-service/src/test/java/org/eclipse/tractusx/managedidentitywallets/revocation/TestUtil.java @@ -93,7 +93,7 @@ public static VerifiableCredentialBuilder mockStatusListVC( new VerifiableCredentialBuilder() .context(VC_CONTEXTS) .id(URI.create(issuer + "#" + index)) - .type(List.of("VerifiableCredential", "BitstringStatusListCredential")) + .type(List.of("VerifiableCredential", "StatusList2021Credential")) .issuer(URI.create(issuer)) .expirationDate(Instant.now().plusSeconds(200000000L)) .issuanceDate(Instant.now()) @@ -149,7 +149,7 @@ public static Map mockStatusList(String encodedList) { Map credentialSubjectMap = new HashMap(); credentialSubjectMap.put( StatusListCredentialSubject.SUBJECT_ID, STATUS_LIST_CREDENTIAL_SUBJECT_ID); - credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_TYPE, "BitstringStatusList"); + credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_TYPE, "StatusList2021"); credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_STATUS_PURPOSE, "revocation"); credentialSubjectMap.put(StatusListCredentialSubject.SUBJECT_ENCODED_LIST, encodedList); return credentialSubjectMap; From 969d6a023d061240c5d2d205acae0837b41f6b3d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 18 Oct 2024 04:55:10 +0000 Subject: [PATCH 58/60] chore(release): 1.0.0-develop.5 [skip ci] # [1.0.0-develop.5](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.4...v1.0.0-develop.5) (2024-10-18) ### Bug Fixes * chart workflows ([3d0fbf9](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/3d0fbf9d18be9f3078e9c427a8f3239ae5a67b53)) * compilation error ([90ef524](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/90ef5241d38e678eff5064c2cd5edb0e6fdc1541)) * copy path in docker file ([ad65e01](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/ad65e0196d40bea81d600c50170bbf31f988eaf9)) * dependencies addded at individual project level ([60e3a5c](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/60e3a5ccf7a77a6efc6428650d9f3091e6002707)) * docker context path ([ce29cb8](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/ce29cb8287293a88745ec2c9bbb5d938564c568a)) * dockerfile ([234a7a0](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/234a7a05d6839409b1a62a1d15ac3a424cfad20c)) * dockerfile and dockerfile location ([042292f](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/042292f1d4ebf6018721af1f748f3598ee619aa9)) * failing test ([a99ca32](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/a99ca32e870a328858913f60c3bae28ca1f315dd)) * failing test cases ([e91b6a0](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/e91b6a0330b65d09bc24b6c38086d17ee16a761c)) * file copy path in Dockerfile ([7d76b00](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/7d76b0002615720ed2c0a1f0d45e2ca5bef642be)) * modefied the value for the replicas ([14a67e1](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/14a67e1b982569ddf6e3becd8176fc346b5ef731)) * more test added ([e739cdc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/e739cdc6be1cc8c0c9d3cf2dde8b1b2ef0055b22)) * random port added for management url ([6b118b2](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/6b118b270ff77f93ee26be8de53706ddba9de277)) * revocation service dockerfile ([28796db](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/28796dbe1642d1255f4cfd309ce3332055abf39a)) * sonar issues ([643493d](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/643493df5b862bbc1a86b30360706180d78aa19e)) * sonar issues ([b1c5417](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/b1c54176cfba899fbcfed32dc1d028cc028e0a68)) * status list changed to 2021 from bitstring ([546908b](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/546908b5e13ce4a0695fa5ade42a14a1abb88a2b)) * status list VS as JSON-LD ([65dd812](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/65dd8124787e7897260e720d42b17e61cdb8955c)) * test cases due to revocation client ([02ccd31](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/02ccd3112bbed9ff14f8f487a5df40eaced79eba)) * tests ([df62fcc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/df62fcc2e5c841a4cfd893d99fad22d90d34e792)) * user added in dockerfile ([44b46ff](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/44b46ff81ae0730946a198fd26de199755f1df77)) * zap scan errors ([074ab2d](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/074ab2df61f94776d23b45eebc87220fbd61a0ef)) * zap scan errors ([c162cad](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/c162cada45cd0a885d0546214a18a32a7ac41f02)) ### Features * Helm charts for revocation service ([badb46d](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/badb46d7d90232d661665a33410c3f11d210a401)) * intial revocation service added ([c173bd4](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/c173bd4d9902d798d75683247d13c89dda6861d2)) * release workflow added for revocation-service ([f70b345](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/f70b3451c357dacd9b63e324efd23018723547f5)) * revoke API, revocation support in issue VC API, wallet-commons module for common classes ([ec8bb00](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/ec8bb008746ee901acccc8eaccda3e5793aea775)) * status list VC type set to StatusList2021 ([4429211](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/4429211d8b3bcb999b35a268f4fe588b9b28ef20)) * test coverage verification added at root gradle level and javadoc for miw-commons ([6a7cff2](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/6a7cff2acb846fa9b664b359ec8fc179673df459)) --- CHANGELOG.md | 39 +++++++++++++++++++++++ charts/managed-identity-wallet/Chart.yaml | 4 +-- charts/managed-identity-wallet/README.md | 2 +- gradle.properties | 2 +- miw/DEPENDENCIES | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83e8bf5..3e7c1598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +# [1.0.0-develop.5](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.4...v1.0.0-develop.5) (2024-10-18) + + +### Bug Fixes + +* chart workflows ([3d0fbf9](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/3d0fbf9d18be9f3078e9c427a8f3239ae5a67b53)) +* compilation error ([90ef524](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/90ef5241d38e678eff5064c2cd5edb0e6fdc1541)) +* copy path in docker file ([ad65e01](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/ad65e0196d40bea81d600c50170bbf31f988eaf9)) +* dependencies addded at individual project level ([60e3a5c](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/60e3a5ccf7a77a6efc6428650d9f3091e6002707)) +* docker context path ([ce29cb8](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/ce29cb8287293a88745ec2c9bbb5d938564c568a)) +* dockerfile ([234a7a0](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/234a7a05d6839409b1a62a1d15ac3a424cfad20c)) +* dockerfile and dockerfile location ([042292f](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/042292f1d4ebf6018721af1f748f3598ee619aa9)) +* failing test ([a99ca32](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/a99ca32e870a328858913f60c3bae28ca1f315dd)) +* failing test cases ([e91b6a0](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/e91b6a0330b65d09bc24b6c38086d17ee16a761c)) +* file copy path in Dockerfile ([7d76b00](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/7d76b0002615720ed2c0a1f0d45e2ca5bef642be)) +* modefied the value for the replicas ([14a67e1](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/14a67e1b982569ddf6e3becd8176fc346b5ef731)) +* more test added ([e739cdc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/e739cdc6be1cc8c0c9d3cf2dde8b1b2ef0055b22)) +* random port added for management url ([6b118b2](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/6b118b270ff77f93ee26be8de53706ddba9de277)) +* revocation service dockerfile ([28796db](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/28796dbe1642d1255f4cfd309ce3332055abf39a)) +* sonar issues ([643493d](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/643493df5b862bbc1a86b30360706180d78aa19e)) +* sonar issues ([b1c5417](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/b1c54176cfba899fbcfed32dc1d028cc028e0a68)) +* status list changed to 2021 from bitstring ([546908b](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/546908b5e13ce4a0695fa5ade42a14a1abb88a2b)) +* status list VS as JSON-LD ([65dd812](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/65dd8124787e7897260e720d42b17e61cdb8955c)) +* test cases due to revocation client ([02ccd31](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/02ccd3112bbed9ff14f8f487a5df40eaced79eba)) +* tests ([df62fcc](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/df62fcc2e5c841a4cfd893d99fad22d90d34e792)) +* user added in dockerfile ([44b46ff](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/44b46ff81ae0730946a198fd26de199755f1df77)) +* zap scan errors ([074ab2d](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/074ab2df61f94776d23b45eebc87220fbd61a0ef)) +* zap scan errors ([c162cad](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/c162cada45cd0a885d0546214a18a32a7ac41f02)) + + +### Features + +* Helm charts for revocation service ([badb46d](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/badb46d7d90232d661665a33410c3f11d210a401)) +* intial revocation service added ([c173bd4](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/c173bd4d9902d798d75683247d13c89dda6861d2)) +* release workflow added for revocation-service ([f70b345](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/f70b3451c357dacd9b63e324efd23018723547f5)) +* revoke API, revocation support in issue VC API, wallet-commons module for common classes ([ec8bb00](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/ec8bb008746ee901acccc8eaccda3e5793aea775)) +* status list VC type set to StatusList2021 ([4429211](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/4429211d8b3bcb999b35a268f4fe588b9b28ef20)) +* test coverage verification added at root gradle level and javadoc for miw-commons ([6a7cff2](https://github.com/eclipse-tractusx/managed-identity-wallet/commit/6a7cff2acb846fa9b664b359ec8fc179673df459)) + # [1.0.0-develop.4](https://github.com/eclipse-tractusx/managed-identity-wallet/compare/v1.0.0-develop.3...v1.0.0-develop.4) (2024-08-09) diff --git a/charts/managed-identity-wallet/Chart.yaml b/charts/managed-identity-wallet/Chart.yaml index 9e31ac68..1d52d7f0 100644 --- a/charts/managed-identity-wallet/Chart.yaml +++ b/charts/managed-identity-wallet/Chart.yaml @@ -25,8 +25,8 @@ description: | type: application -version: 1.0.0-develop.4 -appVersion: 1.0.0-develop.4 +version: 1.0.0-develop.5 +appVersion: 1.0.0-develop.5 home: https://github.com/eclipse-tractusx/managed-identity-wallet keywords: diff --git a/charts/managed-identity-wallet/README.md b/charts/managed-identity-wallet/README.md index 9c524424..004c5912 100644 --- a/charts/managed-identity-wallet/README.md +++ b/charts/managed-identity-wallet/README.md @@ -2,7 +2,7 @@ # managed-identity-wallet -![Version: 1.0.0-develop.4](https://img.shields.io/badge/Version-1.0.0--develop.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.4](https://img.shields.io/badge/AppVersion-1.0.0--develop.4-informational?style=flat-square) +![Version: 1.0.0-develop.5](https://img.shields.io/badge/Version-1.0.0--develop.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0-develop.5](https://img.shields.io/badge/AppVersion-1.0.0--develop.5-informational?style=flat-square) Managed Identity Wallet is supposed to supply a secure data source and data sink for Digital Identity Documents (DID), in order to enable Self-Sovereign Identity founding on those DIDs. And at the same it shall support an uninterrupted tracking and tracing and documenting the usage of those DIDs, e.g. within logistical supply chains. diff --git a/gradle.properties b/gradle.properties index 4e9e6e54..27dcc51a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ jacocoVersion=0.8.9 springBootVersion=3.3.2 springDependencyVersion=1.1.0 groupName=org.eclipse.tractusx -applicationVersion=0.5.0-develop.20 +applicationVersion=1.0.0-develop.5 openApiVersion=2.5.0 lombokVersion=1.18.32 gsonVersion=2.10.1 diff --git a/miw/DEPENDENCIES b/miw/DEPENDENCIES index 9f92211f..c1d00a95 100644 --- a/miw/DEPENDENCIES +++ b/miw/DEPENDENCIES @@ -2,7 +2,7 @@ maven/mavencentral/ch.qos.logback/logback-classic/1.5.6, EPL-1.0 AND LGPL-2.1-on maven/mavencentral/ch.qos.logback/logback-core/1.5.6, EPL-1.0 AND LGPL-2.1-only, approved, #15210 maven/mavencentral/com.apicatalog/titanium-json-ld/1.3.3, Apache-2.0, approved, #8912 maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.2, Apache-2.0, approved, #13672 -maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.2, , approved, #13665 +maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.2, Apache-2.0 AND MIT, approved, #13665 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.2, Apache-2.0, approved, #13671 maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.17.2, Apache-2.0, approved, #13666 maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.17.2, Apache-2.0, approved, #13669 From 3cd6b4023afa610ce3aec676175110968145347c Mon Sep 17 00:00:00 2001 From: Ronak Thacker Date: Fri, 18 Oct 2024 10:43:46 +0530 Subject: [PATCH 59/60] chore: trufflehog workgflow added --- .github/workflows/trufflehog.yml | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/trufflehog.yml diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml new file mode 100644 index 00000000..72e41a6b --- /dev/null +++ b/.github/workflows/trufflehog.yml @@ -0,0 +1,60 @@ +# +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +name: "TruffleHog" + +on: + push: + branches: ["main"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["main"] + schedule: + - cron: "0 0 * * *" # Once a day + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + id-token: write + issues: write + +jobs: + ScanSecrets: + name: Scan secrets + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensure full clone for pull request workflows + + - name: TruffleHog OSS + id: trufflehog + uses: trufflesecurity/trufflehog@8a8ef8526527dd5f5d731d8e74843c121777b82d #v3.80.2 + continue-on-error: true + with: + path: ./ # Scan the entire repository + base: "${{ github.event.repository.default_branch }}" # Set base branch for comparison (pull requests) + extra_args: --filter-entropy=4 --results=verified,unknown --debug + + - name: Scan Results Status + if: steps.trufflehog.outcome == 'failure' + run: exit 1 # Set workflow run to failure if TruffleHog finds secrets From 0ae7f42ceb9a7f5a124c4d327a9e38aca2a553f8 Mon Sep 17 00:00:00 2001 From: Nitin <45592624+nitin-vavdiya@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:55:18 +0530 Subject: [PATCH 60/60] Update trufflehog.yml --only-verified flag added while scanning --- .github/workflows/trufflehog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index 72e41a6b..99ebba07 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -53,7 +53,7 @@ jobs: with: path: ./ # Scan the entire repository base: "${{ github.event.repository.default_branch }}" # Set base branch for comparison (pull requests) - extra_args: --filter-entropy=4 --results=verified,unknown --debug + extra_args: --filter-entropy=4 --results=verified,unknown --debug --only-verified - name: Scan Results Status if: steps.trufflehog.outcome == 'failure'