Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ES-504] added sunbirdrcVciIssuancePlugin #4

Merged
merged 4 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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";

vishwa-vyom marked this conversation as resolved.
Show resolved Hide resolved
@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<String> supportedCredentialTypes;

private final Map<String, Template> credentialTypeTemplates = new HashMap<>();

private final Map<String,Map<String,String>> 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<JsonLDObject> getVerifiableCredentialWithLinkedDataProof(VCRequestDto vcRequestDto, String holderId, Map<String, Object> identityDetails) throws VCIExchangeException {
if (vcRequestDto == null || vcRequestDto.getType() == null) {
throw new VCIExchangeException(ErrorConstants.VCI_EXCHANGE_FAILED);
}
List<String> 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<String> 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<String,Object> responseRegistryMap =fetchRegistryObject(registryUrl+osid);
Map<String,Object> credentialRequestMap = createCredentialIssueRequest(requestedCredentialType, responseRegistryMap,vcRequestDto,holderId);
Map<String,Object> 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<String> getVerifiableCredential(VCRequestDto vcRequestDto, String holderId, Map<String, Object> identityDetails) throws VCIExchangeException {
throw new VCIExchangeException(ErrorConstants.NOT_IMPLEMENTED);
}

private Map<String,Object> fetchRegistryObject(String entityUrl) throws VCIExchangeException {
RequestEntity requestEntity = RequestEntity
vishwa-vyom marked this conversation as resolved.
Show resolved Hide resolved
.get(UriComponentsBuilder.fromUriString(entityUrl).build().toUri()).build();
ResponseEntity<Map<String,Object>> responseEntity = restTemplate.exchange(requestEntity,
new ParameterizedTypeReference<Map<String,Object>>() {});
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<String,Object> createCredentialIssueRequest(String requestedCredentialType, Map<String,Object> registryObjectMap, VCRequestDto vcRequestDto, String holderId) throws VCIExchangeException {

Template template=credentialTypeTemplates.get(requestedCredentialType);
Map<String,String> configMap=credentialTypeConfigMap.get(requestedCredentialType);
StringWriter writer = new StringWriter();
VelocityContext context = new VelocityContext();
Map<String,Object> requestMap=new HashMap<>();
context.put("currentDate", LocalDateTime.now());
context.put("issuerId", configMap.get(STATIC_VALUE_MAP_ISSUER_ID));
for (Map.Entry<String, Object> entry : registryObjectMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof List) {
JSONArray jsonArray = new JSONArray((List<String>) value);
context.put(key, jsonArray);
} else {
context.put(key, value);
}
}
template.merge(context, writer);
try{
Map<String,Object> credentialObject =mapper.readValue(writer.toString(),Map.class);
((Map<String, Object>) 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);
}
vishwa-vyom marked this conversation as resolved.
Show resolved Hide resolved
//TODO This need to be removed since it can contain PII
log.info("VC requset is {}",requestMap);
return requestMap;
}

private Map<String, Object> sendCredentialIssueRequest(Map<String,Object> 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<Map<String,Object>> responseEntity = restTemplate.exchange(requestEntity,
new ParameterizedTypeReference<Map<String,Object>>(){});
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<String,String> 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<String,String> 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
vishwa-vyom marked this conversation as resolved.
Show resolved Hide resolved
credentialTypeTemplates.put(credentialType, template);
}

private void validateContextUrl(Template template,List<String> vcRequestContextList) throws VCIExchangeException {
vishwa-vyom marked this conversation as resolved.
Show resolved Hide resolved
try{
StringWriter writer = new StringWriter();
template.merge(new VelocityContext(),writer);
Map<String,Object> templateMap = mapper.readValue(writer.toString(),Map.class);
List<String> contextList=(List<String>) 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();
}
}
Loading