diff --git a/sunbird-rc-esignet-integration-impl/src/main/java/io/mosip/esignet/sunbirdrc/integration/service/MockVCIssuancePlugin.java b/sunbird-rc-esignet-integration-impl/src/main/java/io/mosip/esignet/sunbirdrc/integration/service/MockVCIssuancePlugin.java deleted file mode 100644 index 7cdab70..0000000 --- a/sunbird-rc-esignet-integration-impl/src/main/java/io/mosip/esignet/sunbirdrc/integration/service/MockVCIssuancePlugin.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.esignet.sunbirdrc.integration.service; - - -import java.util.Map; -import io.mosip.esignet.api.exception.VCIExchangeException; -import io.mosip.esignet.api.util.ErrorConstants; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; -import foundation.identity.jsonld.JsonLDObject; -import io.mosip.esignet.api.dto.VCRequestDto; -import io.mosip.esignet.api.dto.VCResult; -import io.mosip.esignet.api.spi.VCIssuancePlugin; -import lombok.extern.slf4j.Slf4j; - - -@ConditionalOnProperty(value = "mosip.esignet.integration.vci-plugin", havingValue = "SunbirdRCVCIssuancePlugin") -@Component -@Slf4j -public class MockVCIssuancePlugin implements VCIssuancePlugin { - - - @Override - public VCResult getVerifiableCredentialWithLinkedDataProof(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { - throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); - } - - @Override - public VCResult getVerifiableCredential(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { - throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); - } -} diff --git a/sunbird-rc-esignet-integration-impl/src/main/java/io/mosip/esignet/sunbirdrc/integration/service/SunbirdRCVCIssuancePlugin.java b/sunbird-rc-esignet-integration-impl/src/main/java/io/mosip/esignet/sunbirdrc/integration/service/SunbirdRCVCIssuancePlugin.java new file mode 100644 index 0000000..40280be --- /dev/null +++ b/sunbird-rc-esignet-integration-impl/src/main/java/io/mosip/esignet/sunbirdrc/integration/service/SunbirdRCVCIssuancePlugin.java @@ -0,0 +1,267 @@ +/* + * 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.esignet.sunbirdrc.integration.service; + + +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.util.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.esignet.api.exception.VCIExchangeException; +import io.mosip.esignet.api.util.ErrorConstants; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.runtime.RuntimeConstants; +import org.apache.velocity.runtime.resource.loader.URLResourceLoader; +import org.json.JSONArray; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.env.Environment; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import foundation.identity.jsonld.JsonLDObject; +import io.mosip.esignet.api.dto.VCRequestDto; +import io.mosip.esignet.api.dto.VCResult; +import io.mosip.esignet.api.spi.VCIssuancePlugin; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.PostConstruct; + + +@ConditionalOnProperty(value = "mosip.esignet.integration.vci-plugin", havingValue = "SunbirdRCVCIssuancePlugin") +@Component +@Slf4j +public class SunbirdRCVCIssuancePlugin implements VCIssuancePlugin { + + private static final String CREDENTIAL_TYPE_PROPERTY_PREFIX ="mosip.esignet.vciplugin.sunbird-rc.credential-type"; + + private static final String LINKED_DATA_PROOF_VC_FORMAT ="ldp_vc"; + + private static final String TEMPLATE_URL = "template-url"; + + private static final String REGISTRY_GET_URL = "registry-get-url"; + + private static final String CRED_SCHEMA_ID = "cred-schema-id"; + + private static final String CRED_SCHEMA_VESRION = "cred-schema-version"; + + private static final String STATIC_VALUE_MAP_ISSUER_ID = "static-value-map.issuerId"; + + @Autowired + Environment env; + + @Autowired + ObjectMapper mapper; + + @Autowired + private RestTemplate restTemplate; + + @Value("${mosip.esignet.vciplugin.sunbird-rc.issue-credential-url}") + String issueCredentialUrl; + + @Value("#{'${mosip.esignet.vciplugin.sunbird-rc.supported-credential-types}'.split(',')}") + List supportedCredentialTypes; + + private final Map credentialTypeTemplates = new HashMap<>(); + + private final Map> credentialTypeConfigMap = new HashMap<>(); + + private VelocityEngine vEngine; + + + @PostConstruct + public void initialize() throws VCIExchangeException { + + vEngine = new VelocityEngine(); + vEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "url"); + vEngine.setProperty("url.resource.loader.class", URLResourceLoader.class.getName()); + vEngine.init(); + //Validate all the supported VC + for (String credentialType : supportedCredentialTypes) { + validateAndCachePropertiesForCredentialType(credentialType.trim()); + } + } + + @Override + public VCResult getVerifiableCredentialWithLinkedDataProof(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { + if (vcRequestDto == null || vcRequestDto.getType() == null) { + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + List types = vcRequestDto.getType(); + if (types.isEmpty() || !types.get(0).equals("VerifiableCredential")) { + log.error("Invalid request: first item in type is not VerifiableCredential"); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + types.remove(0); + String requestedCredentialType = String.join("-", types); + //Check if the key is in the supported-credential-types + if (!supportedCredentialTypes.contains(requestedCredentialType)) { + log.error("Credential type is not supported"); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + //Validate context of vcrequestdto with template + List contextList=vcRequestDto.getContext(); + for(String supportedType:supportedCredentialTypes){ + Template template=credentialTypeTemplates.get(supportedType); + validateContextUrl(template,contextList); + } + String osid = (identityDetails.containsKey("sub")) ? (String) identityDetails.get("sub") : null; + if (osid == null) { + log.error("Invalid request: osid is null"); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + String registryUrl=credentialTypeConfigMap.get(requestedCredentialType).get(REGISTRY_GET_URL); + Map responseRegistryMap =fetchRegistryObject(registryUrl+osid); + Map credentialRequestMap = createCredentialIssueRequest(requestedCredentialType, responseRegistryMap,vcRequestDto,holderId); + Map vcResponseMap =sendCredentialIssueRequest(credentialRequestMap); + + VCResult vcResult = new VCResult(); + JsonLDObject vcJsonLdObject = JsonLDObject.fromJsonObject(vcResponseMap); + vcResult.setCredential(vcJsonLdObject); + vcResult.setFormat(LINKED_DATA_PROOF_VC_FORMAT); + return vcResult; + } + + + @Override + public VCResult getVerifiableCredential(VCRequestDto vcRequestDto, String holderId, Map identityDetails) throws VCIExchangeException { + throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED); + } + + private Map fetchRegistryObject(String entityUrl) throws VCIExchangeException { + RequestEntity requestEntity = RequestEntity + .get(UriComponentsBuilder.fromUriString(entityUrl).build().toUri()).build(); + ResponseEntity> responseEntity = restTemplate.exchange(requestEntity, + new ParameterizedTypeReference>() {}); + if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null) { + return responseEntity.getBody(); + }else { + log.error("Sunbird service is not running. Status Code: " ,responseEntity.getStatusCode()); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + } + + private Map createCredentialIssueRequest(String requestedCredentialType, Map registryObjectMap, VCRequestDto vcRequestDto, String holderId) throws VCIExchangeException { + + Template template=credentialTypeTemplates.get(requestedCredentialType); + Map configMap=credentialTypeConfigMap.get(requestedCredentialType); + StringWriter writer = new StringWriter(); + VelocityContext context = new VelocityContext(); + Map requestMap=new HashMap<>(); + context.put("currentDate", LocalDateTime.now()); + context.put("issuerId", configMap.get(STATIC_VALUE_MAP_ISSUER_ID)); + for (Map.Entry entry : registryObjectMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof List) { + JSONArray jsonArray = new JSONArray((List) value); + context.put(key, jsonArray); + } else { + context.put(key, value); + } + } + template.merge(context, writer); + try{ + Map credentialObject =mapper.readValue(writer.toString(),Map.class); + ((Map) credentialObject.get("credentialSubject")).put("id", holderId); + requestMap.put("credential", credentialObject); + requestMap.put("credentialSchemaId",configMap.get(CRED_SCHEMA_ID)); + requestMap.put("credentialSchemaVersion",configMap.get(CRED_SCHEMA_VESRION)); + requestMap.put("tags",new ArrayList<>()); + }catch (JsonProcessingException e){ + log.error("Error while parsing the template ",e); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + //TODO This need to be removed since it can contain PII + log.info("VC requset is {}",requestMap); + return requestMap; + } + + private Map sendCredentialIssueRequest(Map credentialRequestMap) throws VCIExchangeException { + try{ + String requestBody=mapper.writeValueAsString(credentialRequestMap); + RequestEntity requestEntity = RequestEntity + .post(UriComponentsBuilder.fromUriString(issueCredentialUrl).build().toUri()) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .body(requestBody); + ResponseEntity> responseEntity = restTemplate.exchange(requestEntity, + new ParameterizedTypeReference>(){}); + if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null){ + //TODO This need to be removed since it can contain PII + log.debug("getting response {}", responseEntity); + return responseEntity.getBody(); + }else{ + log.error("Sunbird service is not running. Status Code: " , responseEntity.getStatusCode()); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + }catch (Exception e){ + log.error("Unable to parse the Registry Object :{}",credentialRequestMap); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + } + + private void validateAndCachePropertiesForCredentialType(String credentialType) throws VCIExchangeException { + Map configMap=new HashMap<>(); + validateAndLoadProperty(CREDENTIAL_TYPE_PROPERTY_PREFIX + "." + credentialType + "." + TEMPLATE_URL,TEMPLATE_URL,configMap); + validateAndLoadProperty(CREDENTIAL_TYPE_PROPERTY_PREFIX + "." + credentialType + "." + REGISTRY_GET_URL,REGISTRY_GET_URL,configMap); + validateAndLoadProperty(CREDENTIAL_TYPE_PROPERTY_PREFIX + "." + credentialType + "." + CRED_SCHEMA_ID,CRED_SCHEMA_ID,configMap); + validateAndLoadProperty(CREDENTIAL_TYPE_PROPERTY_PREFIX + "." + credentialType + "." + CRED_SCHEMA_VESRION,CRED_SCHEMA_VESRION,configMap); + validateAndLoadProperty(CREDENTIAL_TYPE_PROPERTY_PREFIX + "." + credentialType + "." + STATIC_VALUE_MAP_ISSUER_ID,STATIC_VALUE_MAP_ISSUER_ID,configMap); + + String templateUrl = env.getProperty(CREDENTIAL_TYPE_PROPERTY_PREFIX +"." + credentialType + "." + TEMPLATE_URL); + validateAndCacheTemplate(templateUrl,credentialType); + // cache configuration with their credential type + credentialTypeConfigMap.put(credentialType,configMap); + } + + private void validateAndLoadProperty(String propertyName, String credentialProp, Map configMap) throws VCIExchangeException { + String propertyValue = env.getProperty(propertyName); + if (propertyValue == null || propertyValue.isEmpty()) { + throw new VCIExchangeException("Property " + propertyName + " is not set Properly."); + } + configMap.put(credentialProp,propertyValue); + } + + private void validateAndCacheTemplate(String templateUrl, String credentialType){ + Template template = vEngine.getTemplate(templateUrl); + //Todo Validate if all the templates are valid JSON-LD documents + credentialTypeTemplates.put(credentialType, template); + } + + private void validateContextUrl(Template template,List vcRequestContextList) throws VCIExchangeException { + try{ + StringWriter writer = new StringWriter(); + template.merge(new VelocityContext(),writer); + Map templateMap = mapper.readValue(writer.toString(),Map.class); + List contextList=(List) templateMap.get("@context"); + for(String contextUrl:vcRequestContextList){ + if(!contextList.contains(contextUrl)){ + log.error("ContextUrl is not supported"); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + } + }catch ( JsonProcessingException e){ + log.error("Error while parsing the template ",e); + throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED); + } + } + + private static Date calculateNowPlus30Days() { + // Implement your logic to calculate current date + 30 days + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 30); + return calendar.getTime(); + } +}