diff --git a/.github/workflows/push-trigger.yml b/.github/workflows/push-trigger.yml index d8aeed6..6467373 100644 --- a/.github/workflows/push-trigger.yml +++ b/.github/workflows/push-trigger.yml @@ -44,7 +44,7 @@ jobs: secrets: OSSRH_USER: ${{ secrets.OSSRH_USER }} OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} - OSSRH_URL: ${{ secrets.RELEASE_URL }} + OSSRH_URL: ${{ secrets.OSSRH_SNAPSHOT_URL }} OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} @@ -52,7 +52,7 @@ jobs: sonar_analysis_sunbird-rc-esignet-integration-impl: needs: build-maven-sunbird-rc-esignet-integration-impl if: "${{ github.event_name != 'pull_request' }}" - uses: mosip/kattu/.github/workflows/maven-sonar-analysis.yml@master-java21 + uses: mosip/kattu/.github/workflows/maven-sonar-analysis.yml@master with: SERVICE_LOCATION: ./sunbird-rc-esignet-integration-impl secrets: @@ -85,7 +85,7 @@ jobs: secrets: OSSRH_USER: ${{ secrets.OSSRH_USER }} OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} - OSSRH_URL: ${{ secrets.RELEASE_URL }} + OSSRH_URL: ${{ secrets.OSSRH_SNAPSHOT_URL }} OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} @@ -126,7 +126,7 @@ jobs: secrets: OSSRH_USER: ${{ secrets.OSSRH_USER }} OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} - OSSRH_URL: ${{ secrets.RELEASE_URL }} + OSSRH_URL: ${{ secrets.OSSRH_SNAPSHOT_URL }} OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} @@ -145,7 +145,7 @@ jobs: OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - + build-maven-mock-certify-plugin: uses: mosip/kattu/.github/workflows/maven-build.yml@master-java21 with: @@ -167,7 +167,7 @@ jobs: secrets: OSSRH_USER: ${{ secrets.OSSRH_USER }} OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} - OSSRH_URL: ${{ secrets.RELEASE_URL }} + OSSRH_URL: ${{ secrets.OSSRH_SNAPSHOT_URL }} OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} @@ -186,3 +186,44 @@ jobs: OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + build-maven-postgres-dataprovider-plugin: + uses: mosip/kattu/.github/workflows/maven-build.yml@master-java21 + with: + SERVICE_LOCATION: postgres-dataprovider-plugin + BUILD_ARTIFACT: postgres-dataprovider-plugin + secrets: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + publish_to_nexus_postgres-dataprovider-plugin: + if: "${{ !contains(github.ref, 'master') && github.event_name != 'pull_request' && github.event_name != 'release' && github.event_name != 'prerelease' && github.event_name != 'publish' }}" + needs: build-maven-postgres-dataprovider-plugin + uses: mosip/kattu/.github/workflows/maven-publish-to-nexus.yml@master-java21 + with: + SERVICE_LOCATION: ./postgres-dataprovider-plugin + secrets: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_URL: ${{ secrets.OSSRH_SNAPSHOT_URL }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + sonar_analysis_postgres-dataprovider-plugin: + needs: build-maven-postgres-dataprovider-plugin + if: "${{ github.event_name != 'pull_request' }}" + uses: mosip/kattu/.github/workflows/maven-sonar-analysis.yml@master-java21 + with: + SERVICE_LOCATION: ./postgres-dataprovider-plugin + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ORG_KEY: ${{ secrets.ORG_KEY }} + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.gitignore b/.gitignore index f271844..9a6b505 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ target/ .setting/ .mvn/ .project/ +*.DS_Store diff --git a/mock-certify-plugin/pom.xml b/mock-certify-plugin/pom.xml index 828b75f..fc6e540 100644 --- a/mock-certify-plugin/pom.xml +++ b/mock-certify-plugin/pom.xml @@ -10,7 +10,7 @@ io.mosip.certify mock-certify-plugin - 0.2.1-SNAPSHOT + 0.3.0-SNAPSHOT jar mock-certify-integration-impl @@ -54,22 +54,20 @@ 3.6.3 1.3.0-beta.1 - + org.projectlombok lombok 1.18.30 provided - io.mosip.certify certify-core - 0.9.0 + 0.10.0-SNAPSHOT provided - io.mosip.esignet esignet-core @@ -131,6 +129,43 @@ slf4j-api 2.0.12 + + org.jetbrains.kotlinx + kotlinx-datetime-jvm + 0.6.0 + + + com.android.identity + identity-credential + 20231002 + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + co.nstant.in + cbor + 0.9 + + + net.javacrumbs.json-unit + json-unit-assertj + 3.4.1 + test + + + org.apache.commons + commons-csv + 1.9.0 + + + commons-codec + commons-codec + + + @@ -156,6 +191,10 @@ danubetech-maven-public https://repo.danubetech.com/repository/maven-public/ + + google + https://maven.google.com/ + diff --git a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java new file mode 100644 index 0000000..d5542f9 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MDocMockVCIssuancePlugin.java @@ -0,0 +1,156 @@ +package io.mosip.certify.mock.integration.service; + +import foundation.identity.jsonld.JsonLDObject; +import io.mosip.certify.api.dto.VCRequestDto; +import io.mosip.certify.api.dto.VCResult; +import io.mosip.certify.api.exception.VCIExchangeException; +import io.mosip.certify.api.spi.VCIssuancePlugin; +import io.mosip.certify.api.util.ErrorConstants; +import io.mosip.certify.constants.VCFormats; +import io.mosip.certify.core.exception.CertifyException; +import io.mosip.certify.mock.integration.mocks.MdocGenerator; +import io.mosip.esignet.core.dto.OIDCTransaction; +import io.mosip.kernel.core.keymanager.spi.KeyStore; +import io.mosip.kernel.keymanagerservice.constant.KeymanagerConstant; +import io.mosip.kernel.keymanagerservice.entity.KeyAlias; +import io.mosip.kernel.keymanagerservice.helper.KeymanagerDBHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +import javax.crypto.Cipher; +import java.security.Key; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; + +@ConditionalOnProperty(value = "mosip.certify.integration.vci-plugin", havingValue = "MDocMockVCIssuancePlugin") +@Component +@Slf4j +public class MDocMockVCIssuancePlugin implements VCIssuancePlugin { + private static final String AES_CIPHER_FAILED = "aes_cipher_failed"; + private static final String NO_UNIQUE_ALIAS = "no_unique_alias"; + private static final String USERINFO_CACHE = "userinfo"; + + @Autowired + private CacheManager cacheManager; + + @Autowired + private KeyStore keyStore; + + @Autowired + private KeymanagerDBHelper dbHelper; + + @Value("${mosip.certify.cache.security.secretkey.reference-id}") + private String cacheSecretKeyRefId; + + @Value("${mosip.certify.cache.security.algorithm-name}") + private String aesECBTransformation; + + @Value("${mosip.certify.cache.secure.individual-id}") + private boolean secureIndividualId; + + @Value("${mosip.certify.cache.store.individual-id}") + private boolean storeIndividualId; + + @Value("${mosip.certify.mock.vciplugin.mdoc.issuer-key-cert:empty}") + private String issuerKeyAndCertificate = null; + + private static final String ACCESS_TOKEN_HASH = "accessTokenHash"; + + public static final String CERTIFY_SERVICE_APP_ID = "CERTIFY_SERVICE"; + + @Override + public VCResult getVerifiableCredentialWithLinkedDataProof(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { + log.error("not implemented the format {}", vcRequestDto); + throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); + } + + @Override + public VCResult getVerifiableCredential(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { + String accessTokenHash = identityDetails.get(ACCESS_TOKEN_HASH).toString(); + String documentNumber; + try { + documentNumber = getIndividualId(getUserInfoTransaction(accessTokenHash)); + } catch (Exception e) { + log.error("Error getting documentNumber", e); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + + if(vcRequestDto.getFormat().equals(VCFormats.MSO_MDOC)){ + VCResult vcResult = new VCResult<>(); + String mdocVc; + try { + mdocVc = new MdocGenerator().generate(mockDataForMsoMdoc(documentNumber),holderId, issuerKeyAndCertificate); + } catch (Exception e) { + log.error("Exception on mdoc creation", e); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + vcResult.setCredential(mdocVc); + vcResult.setFormat(VCFormats.MSO_MDOC); + return vcResult; + } + log.error("not implemented the format {}", vcRequestDto); + throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); + } + + private Map mockDataForMsoMdoc(String documentNumber) { + Map data = new HashMap<>(); + log.info("Setting up the data for mDoc"); + data.put("family_name","Agatha"); + data.put("given_name","Joseph"); + data.put("birth_date", "1994-11-06"); + data.put("issuing_country", "IN"); + data.put("document_number", documentNumber); + data.put("driving_privileges",new HashMap<>(){{ + put("vehicle_category_code","A"); + }}); + return data; + } + + /** + * TODO: This function getIndividualId is duplicated with Other VCIPlugin class and can be moved to commons + */ + protected String getIndividualId(OIDCTransaction transaction) { + if(!storeIndividualId) + return null; + return secureIndividualId ? decryptIndividualId(transaction.getIndividualId()) : transaction.getIndividualId(); + } + + private String decryptIndividualId(String encryptedIndividualId) { + try { + Cipher cipher = Cipher.getInstance(aesECBTransformation); + byte[] decodedBytes = Base64.getUrlDecoder().decode(encryptedIndividualId); + cipher.init(Cipher.DECRYPT_MODE, getSecretKeyFromHSM()); + return new String(cipher.doFinal(decodedBytes, 0, decodedBytes.length)); + } catch(Exception e) { + log.error("Error Cipher Operations of provided secret data.", e); + throw new CertifyException(AES_CIPHER_FAILED); + } + } + + private OIDCTransaction getUserInfoTransaction(String accessTokenHash) { + return cacheManager.getCache(USERINFO_CACHE).get(accessTokenHash, OIDCTransaction.class); + } + + private Key getSecretKeyFromHSM() { + String keyAlias = getKeyAlias(CERTIFY_SERVICE_APP_ID, cacheSecretKeyRefId); + if (Objects.nonNull(keyAlias)) { + return keyStore.getSymmetricKey(keyAlias); + } + throw new CertifyException(NO_UNIQUE_ALIAS); + } + + private String getKeyAlias(String keyAppId, String keyRefId) { + Map> keyAliasMap = dbHelper.getKeyAliases(keyAppId, keyRefId, LocalDateTime.now(ZoneOffset.UTC)); + List currentKeyAliases = keyAliasMap.get(KeymanagerConstant.CURRENTKEYALIAS); + if (!currentKeyAliases.isEmpty() && currentKeyAliases.size() == 1) { + return currentKeyAliases.getFirst().getAlias(); + } + log.error("CurrentKeyAlias is not unique. KeyAlias count: {}", currentKeyAliases.size()); + throw new CertifyException(NO_UNIQUE_ALIAS); + } +} diff --git a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockCSVDataProviderPlugin.java b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockCSVDataProviderPlugin.java new file mode 100644 index 0000000..1d1506b --- /dev/null +++ b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockCSVDataProviderPlugin.java @@ -0,0 +1,91 @@ +package io.mosip.certify.mock.integration.service; + + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.certify.api.spi.DataProviderPlugin; +import io.mosip.certify.util.CSVReader; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StreamUtils; +import org.springframework.web.client.RestTemplate; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +@ConditionalOnProperty(value = "mosip.certify.integration.data-provider-plugin", havingValue = "MockCSVDataProviderPlugin") +@Component +@Slf4j +public class MockCSVDataProviderPlugin implements DataProviderPlugin { + @Value("${mosip.certify.mock.vciplugin.id-uri:https://example.com/}") + private String id; + @Autowired + private CSVReader csvReader; + @Value("${mosip.certify.mock.data-provider.csv-registry-uri}") + private String csvRegistryURI; + @Value("${mosip.certify.mock.data-provider.csv.identifier-column}") + private String identifierColumn; + @Value("#{'${mosip.certify.mock.data-provider.csv.data-columns}'.split(',')}") + private Set dataColumns; + @Autowired + private RestTemplate restTemplate; + + /** + * initialize sets up a CSV data for this DataProviderPlugin on start of application + * @return + */ + @PostConstruct + public File initialize() throws IOException, JSONException { + File filePath; + if (csvRegistryURI.startsWith("http")) { + // download the file to a path: usecase(docker, spring cloud config) + filePath = restTemplate.execute(csvRegistryURI, HttpMethod.GET, null, resp -> { + File ret = File.createTempFile("download", "tmp"); + StreamUtils.copy(resp.getBody(), new FileOutputStream(ret)); + return ret; + }); + } else if (csvRegistryURI.startsWith("classpath:")) { + try { + // usecase(local setup) + filePath = ResourceUtils.getFile(csvRegistryURI); + } catch (IOException e) { + throw new FileNotFoundException("File not found in: " + csvRegistryURI); + } + } else { + // usecase(local setup) + filePath = new File(csvRegistryURI); + if (!filePath.isFile()) { + // TODO: make sure it crashes the application + throw new FileNotFoundException("File not found: " + csvRegistryURI); + } + } + csvReader.readCSV(filePath, identifierColumn, dataColumns); + return filePath; + } + + @Override + public JSONObject fetchData(Map identityDetails) throws DataProviderExchangeException { + try { + String individualId = (String) identityDetails.get("sub"); + if (individualId != null) { + JSONObject jsonRes = csvReader.getJsonObjectByIdentifier(individualId); + return jsonRes; + } + } catch (Exception e) { + log.error("Failed to fetch json data for from data provider plugin", e); + throw new DataProviderExchangeException("ERROR_FETCHING_IDENTITY_DATA"); + } + throw new DataProviderExchangeException("No Data Found"); + } +} diff --git a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java index 22b7aef..e35ed4b 100644 --- a/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java +++ b/mock-certify-plugin/src/main/java/io.mosip.certify.mock.integration/service/MockVCIssuancePlugin.java @@ -24,8 +24,9 @@ import io.mosip.certify.api.exception.VCIExchangeException; import io.mosip.certify.api.spi.VCIssuancePlugin; import io.mosip.certify.api.util.ErrorConstants; -import io.mosip.certify.core.dto.ParsedAccessToken; +import io.mosip.certify.constants.VCFormats; import io.mosip.certify.core.exception.CertifyException; +import io.mosip.certify.util.UUIDGenerator; import io.mosip.esignet.core.dto.OIDCTransaction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -93,7 +94,7 @@ public class MockVCIssuancePlugin implements VCIssuancePlugin { @Value("#{${mosip.certify.mock.vciplugin.vc-credential-contexts:{'https://www.w3.org/2018/credentials/v1','https://schema.org/'}}}") private List vcCredentialContexts; - private static final String ACCESS_TOKEN_HASH = "accessTokenHash"; + private static final String ACCESS_TOKEN_HASH = "accessTokenHash"; public static final String UTC_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; @@ -107,7 +108,7 @@ public VCResult getVerifiableCredentialWithLinkedDataProof(VCReque VCResult vcResult = new VCResult<>(); vcJsonLdObject = buildJsonLDWithLDProof(identityDetails.get(ACCESS_TOKEN_HASH).toString()); vcResult.setCredential(vcJsonLdObject); - vcResult.setFormat("ldp_vc"); + vcResult.setFormat(VCFormats.LDP_VC); return vcResult; } catch (Exception e) { log.error("Failed to build mock VC", e); @@ -125,10 +126,12 @@ private JsonLDObject buildJsonLDWithLDProof(String accessTokenHash) log.error("Unable to get KYC exchange data from MOCK", e); } + String uuid = new UUIDGenerator().generate(); + Map verCredJsonObject = new HashMap<>(); verCredJsonObject.put("@context", vcCredentialContexts); verCredJsonObject.put("type", Arrays.asList("VerifiableCredential", "MockVerifiableCredential")); - verCredJsonObject.put("id", "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5"); + verCredJsonObject.put("id", uuid); verCredJsonObject.put("issuer", "did:example:123456789"); verCredJsonObject.put("issuanceDate", getUTCDateTime()); verCredJsonObject.put("credentialSubject", formattedMap); @@ -175,7 +178,6 @@ private Map getIndividualData(OIDCTransaction transaction){ ret.put("vcVer", "VC-V1"); ret.put("id", getIdentityUrl+"/"+individualId); ret.put("UIN", individualId); - ret.put("name", res.get("name")); ret.put("fullName", res.get("fullName")); ret.put("gender", res.get("gender")); ret.put("dateOfBirth", res.get("dateOfBirth")); @@ -203,7 +205,7 @@ private String decryptIndividualId(String encryptedIndividualId) { Cipher cipher = Cipher.getInstance(aesECBTransformation); byte[] decodedBytes = Base64.getUrlDecoder().decode(encryptedIndividualId); cipher.init(Cipher.DECRYPT_MODE, getSecretKeyFromHSM()); - return new String(cipher.doFinal(decodedBytes, 0, decodedBytes.length)); + return new String(cipher.doFinal(decodedBytes, 0, decodedBytes.length)); } catch(Exception e) { log.error("Error Cipher Operations of provided secret data.", e); throw new CertifyException(AES_CIPHER_FAILED); @@ -238,7 +240,21 @@ public VCResult getVerifiableCredential(VCRequestDto vcRequestDto, Strin throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); } + private Map mockDataForMsoMdoc(String documentNumber) { + Map data = new HashMap<>(); + log.info("Setting up the data for mDoc"); + data.put("family_name","Agatha"); + data.put("given_name","Joseph"); + data.put("birth_date", "1994-11-06"); + data.put("issuing_country", "IN"); + data.put("document_number", documentNumber); + data.put("driving_privileges",new HashMap<>(){{ + put("vehicle_category_code","A"); + }}); + return data; + } + public OIDCTransaction getUserInfoTransaction(String accessTokenHash) { - return cacheManager.getCache(USERINFO_CACHE).get(accessTokenHash, OIDCTransaction.class); + return cacheManager.getCache(USERINFO_CACHE).get(accessTokenHash, OIDCTransaction.class); } -} +} \ No newline at end of file diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java b/mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java new file mode 100644 index 0000000..86e518b --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/constants/VCFormats.java @@ -0,0 +1,11 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package io.mosip.certify.constants; + +public class VCFormats { + public static final String MSO_MDOC = "mso_mdoc"; + public static final String LDP_VC = "ldp_vc"; +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java b/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java new file mode 100644 index 0000000..3e28593 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/mock/integration/mocks/MdocGenerator.java @@ -0,0 +1,150 @@ +package io.mosip.certify.mock.integration.mocks; + +import co.nstant.in.cbor.CborBuilder; +import co.nstant.in.cbor.CborEncoder; +import co.nstant.in.cbor.CborException; +import co.nstant.in.cbor.model.DataItem; +import com.android.identity.credential.NameSpacedData; +import com.android.identity.internal.Util; +import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator; +import com.android.identity.mdoc.util.MdocUtil; +import com.android.identity.util.Timestamp; +import io.mosip.certify.util.*; + +import java.io.ByteArrayOutputStream; +import java.security.KeyPair; +import java.security.PublicKey; +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.*; + +public class MdocGenerator { + + public static final String NAMESPACE = "org.iso.18013.5.1"; + public static final String DOCTYPE = NAMESPACE + ".mDL"; + public static final String DIGEST_ALGORITHM = "SHA-256"; + public static final String ECDSA_ALGORITHM = "SHA256withECDSA"; + public static final long SEED = 42L; + public static final DateTimeFormatter FULL_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + + /** + * @param data - content of the mdoc + * @param holderId - documentNumber of the mDL + * @param issuerKeyAndCertificate - Document signet related details + * @return + * @throws Exception + * + * As of now, only issuer certificate (DS) is available and its used to sign the mdoc. But as per spec, + * DS certificate is signed by the issuing authority’s root CA certificate basically IA creates a certificate chain keeping the + * root = root CA certificate + * leaf = DS certificate + * And only the DS certificate is attached to the credential. + * Root certificate is not available as of now and is a limitation. + */ + public String generate(Map data, String holderId, String issuerKeyAndCertificate) throws Exception { + KeyPairAndCertificateExtractor keyPairAndCertificateExtractor = new KeyPairAndCertificateExtractor(); + KeyPairAndCertificate issuerDetails = keyPairAndCertificateExtractor.extract(issuerKeyAndCertificate); + + if (issuerDetails.keyPair() == null) { + throw new RuntimeException("Unable to load Crypto details"); + } + + JwkToKeyConverter jwkToKeyConverter = new JwkToKeyConverter(); + PublicKey devicePublicKey = jwkToKeyConverter.convertToPublicKey(holderId.replace("did:jwk:", "")); + KeyPair issuerKeypair = issuerDetails.keyPair(); + + LocalDate issueDate = LocalDate.now(); + String formattedIssueDate = issueDate.format(FULL_DATE_FORMATTER); + LocalDate expiryDate = issueDate.plusYears(5); + String formattedExpiryDate = expiryDate.format(FULL_DATE_FORMATTER); + + NameSpacedData.Builder nameSpacedDataBuilder = new NameSpacedData.Builder(); + nameSpacedDataBuilder.putEntryString(NAMESPACE, "issue_date", formattedIssueDate); + nameSpacedDataBuilder.putEntryString(NAMESPACE, "expiry_date", formattedExpiryDate); + + Map drivingPrivileges = (Map) data.get("driving_privileges"); + drivingPrivileges.put("issue_date", formattedIssueDate); + drivingPrivileges.put("expiry_date", formattedExpiryDate); + + data.keySet().forEach(key -> nameSpacedDataBuilder.putEntryString(NAMESPACE, key, Objects.toString(data.get(key),""))); + + NameSpacedData nameSpacedData = nameSpacedDataBuilder.build(); + Map> generatedIssuerNameSpaces = MdocUtil.generateIssuerNameSpaces(nameSpacedData, new Random(SEED), 16); + Map calculateDigestsForNameSpace = MdocUtil.calculateDigestsForNameSpace(NAMESPACE, generatedIssuerNameSpaces, DIGEST_ALGORITHM); + + MobileSecurityObjectGenerator mobileSecurityObjectGenerator = new MobileSecurityObjectGenerator(DIGEST_ALGORITHM, DOCTYPE, devicePublicKey); + mobileSecurityObjectGenerator.addDigestIdsForNamespace(NAMESPACE, calculateDigestsForNameSpace); + + Timestamp currentTimestamp = Timestamp.now(); + Timestamp validUntil = Timestamp.ofEpochMilli(addYearsToDate(currentTimestamp.toEpochMilli(), 2)); + mobileSecurityObjectGenerator.setValidityInfo(currentTimestamp, currentTimestamp, validUntil, null); + + byte[] mso = mobileSecurityObjectGenerator.generate(); + + DataItem coseSign1Sign = Util.coseSign1Sign( + issuerKeypair.getPrivate(), + ECDSA_ALGORITHM, + Util.cborEncode(Util.cborBuildTaggedByteString(mso)), + null, + Collections.singletonList(issuerDetails.certificate()) + ); + + return construct(generatedIssuerNameSpaces, coseSign1Sign); + } + + private String construct(Map> nameSpaces, DataItem issuerAuth) throws CborException { + MDoc mDoc = new MDoc(DOCTYPE, new IssuerSigned(nameSpaces, issuerAuth)); + byte[] cbor = mDoc.toCBOR(); + return Base64.getUrlEncoder().encodeToString(cbor); + } + + private long addYearsToDate(long dateInEpochMillis, int years) { + Instant instant = Instant.ofEpochMilli(dateInEpochMillis); + Instant futureInstant = instant.plus(years * 365L, ChronoUnit.DAYS); + return futureInstant.toEpochMilli(); + } +} + + +class MDoc { + private final String docType; + private final IssuerSigned issuerSigned; + + public MDoc(String docType, IssuerSigned issuerSigned) { + this.docType = docType; + this.issuerSigned = issuerSigned; + } + + public byte[] toCBOR() throws CborException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + CborEncoder cborEncoder = new CborEncoder(byteArrayOutputStream); + cborEncoder.encode( + new CborBuilder().addMap() + .put("docType", docType) + .put(CBORConverter.toDataItem("issuerSigned"), CBORConverter.toDataItem(issuerSigned.toMap())) + .end() + .build() + ); + return byteArrayOutputStream.toByteArray(); + } +} + +class IssuerSigned { + private final Map> nameSpaces; + private final DataItem issuerAuth; + + public IssuerSigned(Map> nameSpaces, DataItem issuerAuth) { + this.nameSpaces = nameSpaces; + this.issuerAuth = issuerAuth; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("nameSpaces", CBORConverter.toDataItem(nameSpaces)); + map.put("issuerAuth", issuerAuth); + return map; + } +} + diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.java new file mode 100644 index 0000000..418c50c --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CBORConverter.java @@ -0,0 +1,65 @@ +package io.mosip.certify.util; + +import co.nstant.in.cbor.CborDecoder; +import co.nstant.in.cbor.model.Array; +import co.nstant.in.cbor.model.DataItem; +import co.nstant.in.cbor.model.Map; +import co.nstant.in.cbor.model.SimpleValue; +import co.nstant.in.cbor.model.UnicodeString; +import co.nstant.in.cbor.model.UnsignedInteger; + +import java.util.List; + +public class CBORConverter { + + public static DataItem toDataItem(Object value) { + if (value instanceof DataItem) { + return (DataItem) value; + } else if (value instanceof String) { + return new UnicodeString((String) value); + } else if (value instanceof Integer) { + return new UnsignedInteger(((Integer) value).longValue()); + } else if (value instanceof Long) { + return new UnsignedInteger((Long) value); + } else if (value instanceof Boolean) { + return ((Boolean) value) ? SimpleValue.TRUE : SimpleValue.FALSE; + } else if (value instanceof java.util.Map) { + Map cborMap = new Map(); + java.util.Map map = (java.util.Map) value; + for (java.util.Map.Entry entry : map.entrySet()) { + cborMap.put(new UnicodeString((String) entry.getKey()), toDataItem(entry.getValue())); + } + return cborMap; + } else if (value instanceof List) { + Array cborArray = new Array(); + List list = (List) value; + for (Object item : list) { + cborArray.add(toDataItem(item)); + } + return cborArray; + } else if (value instanceof Object[]) { + Array cborArray = new Array(); + Object[] array = (Object[]) value; + for (Object item : array) { + cborArray.add(toDataItem(item)); + } + return cborArray; + } else if (value instanceof byte[]) { + try { + List dataItems = CborDecoder.decode((byte[]) value); + if (!dataItems.isEmpty()) { + return dataItems.get(0); + } + } catch (Exception e) { + throw new IllegalArgumentException("Failed to decode ByteArray", e); + } + } else { + throw new IllegalArgumentException("Unsupported value: " + value + " " + value.getClass().getSimpleName()); + } + + return null; + } +} + + + diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/CSVReader.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CSVReader.java new file mode 100644 index 0000000..b161f2f --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/CSVReader.java @@ -0,0 +1,63 @@ +package io.mosip.certify.util; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; + +@Component +@Slf4j +public class CSVReader { + private Map dataMap = new HashMap<>(); + + public void readCSV(File filePath, String identifierColumn, Set dataColumns) throws IOException, JSONException { + try (FileReader reader = new FileReader(filePath); + CSVParser csvParser = new CSVParser(reader, CSVFormat.DEFAULT.withFirstRecordAsHeader())) { + // Get header names + List headers = csvParser.getHeaderNames(); + // Validate that identifier column exists + if (!headers.contains(identifierColumn)) { + throw new IllegalArgumentException("Identifier column " + identifierColumn + " not found in CSV"); + } + + // Process each record + for (CSVRecord record : csvParser) { + String identifier = record.get(identifierColumn); + JSONObject jsonObject = new JSONObject(); + // Store only the configured fields + for (String header : headers) { + if (dataColumns.contains(header) || header.equals(identifierColumn)) { + jsonObject.put(header, record.get(header)); + } + } + + // Add to dataMap + dataMap.put(identifier, jsonObject); + } + } catch (IOException e) { + log.error("Error finding csv file path", e); + throw new IOException("Unable to find the CSV file."); + } + } + + public JSONObject getJsonObjectByIdentifier(String identifier) throws DataProviderExchangeException, JSONException { + JSONObject record = dataMap.get(identifier); + if(record == null) { + log.error("No identifier found."); + throw new DataProviderExchangeException("No record found in csv with the provided identifier"); + } + + return record; + } +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java new file mode 100644 index 0000000..d7e8a33 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/JwkToKeyConverter.java @@ -0,0 +1,28 @@ +package io.mosip.certify.util; + +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; + +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.util.Base64; + + +public class +JwkToKeyConverter { + + public PublicKey convertToPublicKey(String encodedData) throws Exception { + String jwkJsonString = new String(Base64.getUrlDecoder().decode(encodedData), StandardCharsets.UTF_8); + JWK jwk = JWK.parse(jwkJsonString); + + if (jwk instanceof RSAKey) { + return ((RSAKey) jwk).toPublicKey(); + } else if (jwk instanceof ECKey) { + return ((ECKey) jwk).toPublicKey(); + } + + throw new IllegalArgumentException("Unsupported key type"); + } + +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificate.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificate.java new file mode 100644 index 0000000..c95e10f --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificate.java @@ -0,0 +1,7 @@ +package io.mosip.certify.util; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +public record KeyPairAndCertificate(KeyPair keyPair, X509Certificate certificate) { +} diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificateExtractor.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificateExtractor.java new file mode 100644 index 0000000..398b625 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/KeyPairAndCertificateExtractor.java @@ -0,0 +1,49 @@ +package io.mosip.certify.util; + + +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +@Slf4j +public class KeyPairAndCertificateExtractor { + public KeyPairAndCertificate extract(String keyCert) { + String[] splitKeyCert = keyCert.split("\\|\\|"); + try { + X509Certificate certificate = convertStringToX509Certificate((splitKeyCert[1])); + return (new KeyPairAndCertificate(getKeyPair(splitKeyCert[0], certificate), certificate)); + } catch (Exception e) { + log.error("Failed to extract key certificate", e); + } + + return null; + } + + private X509Certificate convertStringToX509Certificate(String certString) throws CertificateException { + byte[] certBytes = Base64.getDecoder().decode(certString); + + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certBytes)); + } + + private KeyPair getKeyPair(String base64PrivateKey, X509Certificate certificate) throws NoSuchAlgorithmException, InvalidKeySpecException { + byte[] privateKeyBytes = Base64.getDecoder().decode(base64PrivateKey); + + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + + PublicKey publicKey = certificate.getPublicKey(); + + return new KeyPair(publicKey, privateKey); + } +} + + diff --git a/mock-certify-plugin/src/main/java/io/mosip/certify/util/UUIDGenerator.java b/mock-certify-plugin/src/main/java/io/mosip/certify/util/UUIDGenerator.java new file mode 100644 index 0000000..f95ec22 --- /dev/null +++ b/mock-certify-plugin/src/main/java/io/mosip/certify/util/UUIDGenerator.java @@ -0,0 +1,9 @@ +package io.mosip.certify.util; + +import java.util.UUID; + +public class UUIDGenerator { + public String generate() { + return "urn:uuid:" + UUID.randomUUID(); + } +} diff --git a/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockCSVDataProviderPluginTest.java b/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockCSVDataProviderPluginTest.java new file mode 100644 index 0000000..eb1d20b --- /dev/null +++ b/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockCSVDataProviderPluginTest.java @@ -0,0 +1,67 @@ +package io.mosip.certify.mock.integration.service; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.certify.util.CSVReader; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@RunWith(MockitoJUnitRunner.class) +public class MockCSVDataProviderPluginTest { + @Mock + CSVReader csvReader; + + @InjectMocks + MockCSVDataProviderPlugin mockCSVDataProviderPlugin = new MockCSVDataProviderPlugin(); + + @Before + public void setup() throws JSONException, DataProviderExchangeException { + String dataColumnFields = "name,age,phone"; + Set dataColumns = new HashSet<>(Arrays.asList(dataColumnFields.split(","))); + ReflectionTestUtils.setField(mockCSVDataProviderPlugin, "identifierColumn", "individualId"); + ReflectionTestUtils.setField(mockCSVDataProviderPlugin, "dataColumns", dataColumns); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("individualId", "1234567"); + jsonObject.put("name", "John Doe"); + jsonObject.put("age", "40"); + jsonObject.put("phone", "98765"); + + Mockito.when(csvReader.getJsonObjectByIdentifier("1234567")).thenReturn(jsonObject); + } + + @Test + public void fetchJsonDataWithValidIndividualId_thenPass() throws DataProviderExchangeException, JSONException { + JSONObject jsonObject = mockCSVDataProviderPlugin.fetchData(Map.of("sub", "1234567", "client_id", "CLIENT_ID")); + Assert.assertNotNull(jsonObject); + Assert.assertNotNull(jsonObject.get("name")); + Assert.assertNotNull(jsonObject.get("phone")); + Assert.assertNotNull(jsonObject.get("age")); + Assert.assertNotNull(jsonObject.get("individualId")); + Assert.assertEquals("John Doe", jsonObject.get("name")); + Assert.assertEquals("98765", jsonObject.get("phone")); + Assert.assertEquals("40", jsonObject.get("age")); + Assert.assertEquals("1234567", jsonObject.get("individualId")); + } + + @Test + public void fetchJsonDataWithInValidIndividualId_thenFail() { + try { + mockCSVDataProviderPlugin.fetchData(Map.of("sub", "12345678", "client_id", "CLIENT_ID")); + } catch (DataProviderExchangeException e) { + Assert.assertEquals("ERROR_FETCHING_IDENTITY_DATA", e.getMessage()); + } + } +} diff --git a/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java b/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java index b4c91fe..0614e33 100644 --- a/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java +++ b/mock-certify-plugin/src/test/java/io/mosip/certify/mock/integration/service/MockVCIssuancePluginTest.java @@ -4,6 +4,7 @@ import io.mosip.certify.api.dto.VCRequestDto; import io.mosip.certify.api.dto.VCResult; import io.mosip.certify.api.exception.VCIExchangeException; +import io.mosip.certify.core.dto.ParsedAccessToken; import io.mosip.esignet.core.dto.OIDCTransaction; import io.mosip.kernel.signature.dto.JWTSignatureResponseDto; import io.mosip.kernel.signature.service.SignatureService; diff --git a/mock-certify-plugin/src/test/java/io/mosip/certify/util/CSVReaderTest.java b/mock-certify-plugin/src/test/java/io/mosip/certify/util/CSVReaderTest.java new file mode 100644 index 0000000..9a872ab --- /dev/null +++ b/mock-certify-plugin/src/test/java/io/mosip/certify/util/CSVReaderTest.java @@ -0,0 +1,85 @@ +package io.mosip.certify.util; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import lombok.SneakyThrows; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +@RunWith(MockitoJUnitRunner.class) +public class CSVReaderTest { + @InjectMocks + CSVReader csvReader; + + Set dataColumns; + Map dataMap = new HashMap<>(); + + @Before + public void setup() { + String dataColumnFields = "name,age,phone"; + dataColumns = new HashSet<>(Arrays.asList(dataColumnFields.split(","))); + ReflectionTestUtils.setField(csvReader, "dataMap", dataMap); + } + + @Test + public void readCSVDataFromValidFile_thenPass() throws IOException, JSONException { + File f = new File("src/test/resources/test.csv"); + csvReader.readCSV(f, "individualId", dataColumns); + Assert.assertNotNull(dataMap); + Assert.assertNotNull(dataMap.get("1234567")); + JSONObject jsonObject = dataMap.get("1234567"); + Assert.assertNotNull(jsonObject); + Assert.assertEquals("John Doe", jsonObject.get("name")); + Assert.assertEquals("9876543210", jsonObject.get("phone")); + Assert.assertEquals("40", jsonObject.get("age")); + } + + @Test + public void readCSVDataFromInvalidFile_thenFail() throws JSONException { + try { + File f = new File("test.csv"); + csvReader.readCSV(f, "individualId", dataColumns); + } catch (IOException e) { + Assert.assertEquals("Unable to find the CSV file.", e.getMessage()); + } + } + + @SneakyThrows + @Test + public void getJsonObjectByValidIdentifier_thenPass() { + Map data = new HashMap<>(); + JSONObject jsonObject = new JSONObject(Map.of("phone", "9876543210", "name", "John Doe", "individualId", "1234567", "age", "40")); + data.put("1234567", jsonObject); + ReflectionTestUtils.setField(csvReader, "dataMap", data); + + JSONObject jsonObjectResult = csvReader.getJsonObjectByIdentifier("1234567"); + Assert.assertNotNull(jsonObject); + Assert.assertEquals("1234567", jsonObjectResult.get("individualId")); + Assert.assertEquals("John Doe", jsonObjectResult.get("name")); + Assert.assertEquals("40", jsonObjectResult.get("age")); + Assert.assertEquals("9876543210", jsonObjectResult.get("phone")); + } + + @Test + public void getJsonObjectByInvalidIdentifier_thenFail() throws JSONException { + Map data = new HashMap<>(); + JSONObject jsonObject = new JSONObject(Map.of("phone", "9876543210", "name", "John Doe", "individualId", "1234567", "age", "40")); + data.put("1234567", jsonObject); + ReflectionTestUtils.setField(csvReader, "dataMap", data); + try { + csvReader.getJsonObjectByIdentifier("12345678"); + } catch (DataProviderExchangeException e) { + Assert.assertEquals("No record found in csv with the provided identifier", e.getMessage()); + } + } +} diff --git a/mock-certify-plugin/src/test/java/io/mosip/certify/util/UUIDGeneratorTest.java b/mock-certify-plugin/src/test/java/io/mosip/certify/util/UUIDGeneratorTest.java new file mode 100644 index 0000000..1257e12 --- /dev/null +++ b/mock-certify-plugin/src/test/java/io/mosip/certify/util/UUIDGeneratorTest.java @@ -0,0 +1,14 @@ +package io.mosip.certify.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UUIDGeneratorTest { + @Test + void shouldReturnUUIDInRequiredFormatWhenGenerated() { + String generatedUUID = new UUIDGenerator().generate(); + + assertTrue(generatedUUID.matches("^urn:uuid:[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$")); + } +} diff --git a/mock-certify-plugin/src/test/resources/test.csv b/mock-certify-plugin/src/test/resources/test.csv new file mode 100644 index 0000000..933fad3 --- /dev/null +++ b/mock-certify-plugin/src/test/resources/test.csv @@ -0,0 +1,6 @@ +individualId,name,phone,age +1234567,John Doe,9876543210,40 +9876543,Mary Smith,8765432109,45 +2345678,Raj Patel,7654321098,28 +8765432,Sarah Johnson,6543210987,35 + diff --git a/mosip-identity-certify-plugin/pom.xml b/mosip-identity-certify-plugin/pom.xml index 633e53d..765b527 100644 --- a/mosip-identity-certify-plugin/pom.xml +++ b/mosip-identity-certify-plugin/pom.xml @@ -8,7 +8,7 @@ 4.0.0 io.mosip.certify mosip-identity-certify-plugin - 0.2.1-SNAPSHOT + 0.3.0-SNAPSHOT jar mosipid-certify-integration-impl @@ -98,13 +98,13 @@ io.mosip.certify certify-core - 0.9.0 + 0.10.0-SNAPSHOT provided io.mosip.certify certify-integration-api - 0.9.0 + 0.10.0-SNAPSHOT provided @@ -145,6 +145,7 @@ io.mosip.esignet esignet-core + 1.4.1 diff --git a/postgres-dataprovider-plugin/pom.xml b/postgres-dataprovider-plugin/pom.xml new file mode 100644 index 0000000..2586c15 --- /dev/null +++ b/postgres-dataprovider-plugin/pom.xml @@ -0,0 +1,313 @@ + + 4.0.0 + + io.mosip.certify + postgres-dataprovider-plugin + 0.3.0-SNAPSHOT + jar + + postgres-dataprovider-plugin + Data provider plugin implementation through postgres db that is used to showcase the integration with certify + https://github.com/mosip/digital-credential-plugins + + + + MPL 2.0 + https://www.mozilla.org/en-US/MPL/2.0/ + + + + scm:git:git://github.com/mosip/digital-credential-plugins.git + scm:git:ssh://github.com:mosip/digital-credential-plugins.git + https://github.com/mosip/digital-credential-plugins + HEAD + + + + MOSIP + mosip.emailnotifier@gmail.com + io.mosip + https://www.mosip.io + + + + + UTF-8 + 21 + 3.7.1 + 21 + 21 + 3.10.1 + 3.2.5 + 1.5 + 2.2.1 + 6.1.0 + 3.0.1 + 0.8.11 + 3.6.3 + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + io.mosip.certify + certify-core + 0.10.0-SNAPSHOT + provided + + + org.mockito + mockito-core + 5.11.0 + test + + + org.springframework + spring-test + 6.1.4 + + + org.slf4j + slf4j-api + 2.0.12 + + + junit + junit + 4.13.1 + test + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + + + + + + ossrh + CentralRepository + https://oss.sonatype.org/content/repositories/snapshots + default + + true + + + + central + MavenCentral + default + https://repo1.maven.org/maven2 + + false + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + none + src/main/java + + + + maven-deploy-plugin + 3.1.2 + + + default-deploy + deploy + + deploy + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.14 + true + + + default-deploy + deploy + + deploy + + + + + ossrh + https://oss.sonatype.org/ + false + + + + + org.apache.maven.plugins + maven-source-plugin + true + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + pl.project13.maven + git-commit-id-plugin + 3.0.1 + + + get-the-git-infos + + revision + + validate + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.build.(time|version)$ + ^git.commit.id.(abbrev|full)$ + + full + ${project.basedir}/.git + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + true + + ${argLine} --add-opens + java.xml/jdk.xml.internal=ALL-UNNAMED + --illegal-access=permit + + + + + org.jacoco + jacoco-maven-plugin + ${maven.jacoco.version} + + + + prepare-agent + + + + report + prepare-package + + report + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + make-jar-executable + package + + run + + + + + + + + + + + + + \ No newline at end of file diff --git a/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/repository/DataProviderRepository.java b/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/repository/DataProviderRepository.java new file mode 100644 index 0000000..29daaf3 --- /dev/null +++ b/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/repository/DataProviderRepository.java @@ -0,0 +1,8 @@ +package io.mosip.certify.postgresdataprovider.integration.repository; + + +import java.util.Map; + +public interface DataProviderRepository { + Map fetchQueryResult(String id, String queryString); +} \ No newline at end of file diff --git a/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/repository/DataProviderRepositoryImpl.java b/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/repository/DataProviderRepositoryImpl.java new file mode 100644 index 0000000..4a6dcc4 --- /dev/null +++ b/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/repository/DataProviderRepositoryImpl.java @@ -0,0 +1,37 @@ +package io.mosip.certify.postgresdataprovider.integration.repository; + +import jakarta.persistence.*; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +@Repository(value = "dataProviderRepository") +public class DataProviderRepositoryImpl implements DataProviderRepository { + @PersistenceContext + private EntityManager entityManager; + + @Override + public Map fetchQueryResult(String id, String queryString) { + Query query = entityManager.createNativeQuery(queryString, Tuple.class); + query.setParameter("id", id); + List list = query.getResultList(); + List> result = convertTuplesToMap(list); + return result.getFirst(); + } + + public static List> convertTuplesToMap(List tuples) { + List> result = new ArrayList<>(); + for (Tuple single : tuples) { + Map tempMap = new HashMap<>(); + for (TupleElement key : single.getElements()) { + tempMap.put(key.getAlias(), single.get(key)); + } + result.add(tempMap); + } + return result; + } +} \ No newline at end of file diff --git a/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/service/PostgresDataProviderPlugin.java b/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/service/PostgresDataProviderPlugin.java new file mode 100644 index 0000000..e661dd7 --- /dev/null +++ b/postgres-dataprovider-plugin/src/main/java/io/mosip/certify/postgresdataprovider/integration/service/PostgresDataProviderPlugin.java @@ -0,0 +1,49 @@ +package io.mosip.certify.postgresdataprovider.integration.service; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.certify.api.spi.DataProviderPlugin; +import io.mosip.certify.postgresdataprovider.integration.repository.DataProviderRepository; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.util.*; + +@ConditionalOnProperty(value = "mosip.certify.integration.data-provider-plugin", havingValue = "PostgresDataProviderPlugin") +@Component +@Slf4j +public class PostgresDataProviderPlugin implements DataProviderPlugin { + + @Autowired + private DataProviderRepository dataProviderRepository; + + @Autowired + private ObjectMapper objectMapper; + + @Value("#{${mosip.certify.data-provider-plugin.postgres.scope-query-mapping}}") + private LinkedHashMap scopeQueryMapping; + + @Override + public JSONObject fetchData(Map identityDetails) throws DataProviderExchangeException { + try { + String individualId = (String) identityDetails.get("sub"); + String scope = (String) identityDetails.get("scope"); + String queryString = scopeQueryMapping.get(scope); + if (individualId != null) { + Map dataRecord = dataProviderRepository.fetchQueryResult(individualId, + queryString); + JSONObject jsonResponse = new JSONObject(dataRecord); + return jsonResponse; + } + } catch (Exception e) { + log.error("Failed to fetch json data for from data provider plugin", e); + throw new DataProviderExchangeException("ERROR_FETCHING_DATA_RECORD_FROM_TABLE"); + } + throw new DataProviderExchangeException("No Data Found"); + } +} \ No newline at end of file diff --git a/postgres-dataprovider-plugin/src/test/java/io/mosip/certify/postgresdataprovider/integration/PostgresDataProviderPluginTest.java b/postgres-dataprovider-plugin/src/test/java/io/mosip/certify/postgresdataprovider/integration/PostgresDataProviderPluginTest.java new file mode 100644 index 0000000..3a8f3d5 --- /dev/null +++ b/postgres-dataprovider-plugin/src/test/java/io/mosip/certify/postgresdataprovider/integration/PostgresDataProviderPluginTest.java @@ -0,0 +1,71 @@ +package io.mosip.certify.postgresdataprovider.integration; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.certify.postgresdataprovider.integration.repository.DataProviderRepository; +import io.mosip.certify.postgresdataprovider.integration.service.PostgresDataProviderPlugin; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.LinkedHashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class PostgresDataProviderPluginTest { + @Mock + DataProviderRepository dataProviderRepository; + + @InjectMocks + PostgresDataProviderPlugin postgresDataProviderPlugin = new PostgresDataProviderPlugin(); + + @Before + public void setup() { + LinkedHashMap scopeQueryMapping = new LinkedHashMap<>(); + scopeQueryMapping.put("test_vc_ldp", "test_query"); + ReflectionTestUtils.setField(postgresDataProviderPlugin, "scopeQueryMapping", scopeQueryMapping); + Map queryResult = Map.of("id","1234567", "name", "John Doe", "dateOfBirth", "01/01/1980", "phoneNumber", "012345", "email", "john@test.com", "landArea", 100.24); + Mockito.when(dataProviderRepository.fetchQueryResult("1234567", "test_query")).thenReturn(queryResult); + } + + @Test + public void fetchJsonDataWithValidIndividualId_thenPass() throws DataProviderExchangeException, JSONException { + JSONObject jsonObject = postgresDataProviderPlugin.fetchData(Map.of("sub", "1234567", "client_id", "CLIENT_ID", "scope", "test_vc_ldp")); + Assert.assertNotNull(jsonObject); + Assert.assertNotNull(jsonObject.get("name")); + Assert.assertNotNull(jsonObject.get("dateOfBirth")); + Assert.assertNotNull(jsonObject.get("phoneNumber")); + Assert.assertNotNull(jsonObject.get("email")); + Assert.assertNotNull(jsonObject.get("landArea")); + Assert.assertEquals("John Doe", jsonObject.get("name")); + Assert.assertEquals("01/01/1980", jsonObject.get("dateOfBirth")); + Assert.assertEquals("012345", jsonObject.get("phoneNumber")); + Assert.assertEquals("john@test.com", jsonObject.get("email")); + Assert.assertEquals(100.24, jsonObject.get("landArea")); + } + + @Test + public void fetchJsonDataWithInValidIndividualId_thenFail() throws DataProviderExchangeException, JSONException { + try { + postgresDataProviderPlugin.fetchData(Map.of("sub", "12345678", "client_id", "CLIENT_ID", "scope", "test_vc_ldp")); + } catch (DataProviderExchangeException e) { + Assert.assertEquals("ERROR_FETCHING_DATA_RECORD_FROM_TABLE", e.getMessage()); + } + } + + @Test + public void fetchJsonDataWithInValidScope_thenFail() throws DataProviderExchangeException, JSONException { + try { + postgresDataProviderPlugin.fetchData(Map.of("sub", "1234567", "client_id", "CLIENT_ID", "scope", "sample_vc_ldp")); + } catch (DataProviderExchangeException e) { + Assert.assertEquals("ERROR_FETCHING_DATA_RECORD_FROM_TABLE", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/sunbird-rc-certify-integration-impl/pom.xml b/sunbird-rc-certify-integration-impl/pom.xml index b043068..6af0383 100644 --- a/sunbird-rc-certify-integration-impl/pom.xml +++ b/sunbird-rc-certify-integration-impl/pom.xml @@ -8,7 +8,7 @@ 4.0.0 io.mosip.certify.sunbirdrc sunbird-rc-certify-integration-impl - 0.2.1-SNAPSHOT + 0.3.0-SNAPSHOT jar sunbird-rc-certify-integration-impl @@ -54,7 +54,7 @@ io.mosip.certify certify-integration-api - 0.9.0 + 0.10.0-SNAPSHOT provided diff --git a/sunbird-rc-esignet-integration-impl/pom.xml b/sunbird-rc-esignet-integration-impl/pom.xml index 65b710c..dd497e7 100644 --- a/sunbird-rc-esignet-integration-impl/pom.xml +++ b/sunbird-rc-esignet-integration-impl/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.mosip.esignet.sunbirdrc sunbird-rc-esignet-integration-impl - 0.2.1-SNAPSHOT + 0.3.0-SNAPSHOT jar sunbird-rc-esignet-integration-impl