From 4db08d0bea34f4daf4ad6f81d460c466c65e2589 Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Wed, 7 Dec 2022 21:11:59 +0530 Subject: [PATCH 1/9] feat: consent request asynchronous framework --- java/consent/HELP.md | 9 + java/consent/pom.xml | 92 +++++++++ .../sunbirdrc/consent/ConsentApplication.java | 13 ++ .../consent/constants/ConstentStatus.java | 5 + .../consent/controller/ConsentController.java | 44 +++++ .../dev/sunbirdrc/consent/entity/Consent.java | 75 ++++++++ .../ConsentDefinitionNotFoundException.java | 7 + .../consent/repository/ConsentRepository.java | 9 + .../consent/service/ConsentService.java | 37 ++++ .../src/main/resources/application.properties | 11 ++ .../consent/ConsentApplicationTests.java | 13 ++ .../sunbirdrc/pojos/PluginRequestMessage.java | 5 + .../dev/sunbirdrc/pojos/dto/ConsentDTO.java | 16 ++ java/pom.xml | 3 +- .../controller/AbstractController.java | 8 + .../controller/RegistryConsentController.java | 176 ++++++++++++++++++ .../controller/RegistryEntityController.java | 62 +----- .../exception/ConsentForbiddenException.java | 7 + .../registry/util/ConsentRequestClient.java | 82 ++++++++ .../sunbirdrc/actors/ConsentPluginActor.java | 52 ++++++ .../src/main/resources/sunbirdrc-actors.conf | 5 + 21 files changed, 669 insertions(+), 62 deletions(-) create mode 100644 java/consent/HELP.md create mode 100644 java/consent/pom.xml create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/ConsentApplication.java create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/constants/ConstentStatus.java create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentDefinitionNotFoundException.java create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java create mode 100644 java/consent/src/main/resources/application.properties create mode 100644 java/consent/src/test/java/dev/sunbirdrc/consent/ConsentApplicationTests.java create mode 100644 java/pojos/src/main/java/dev/sunbirdrc/pojos/dto/ConsentDTO.java create mode 100644 java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java create mode 100644 java/registry/src/main/java/dev/sunbirdrc/registry/exception/ConsentForbiddenException.java create mode 100644 java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java create mode 100644 java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java diff --git a/java/consent/HELP.md b/java/consent/HELP.md new file mode 100644 index 000000000..06131a596 --- /dev/null +++ b/java/consent/HELP.md @@ -0,0 +1,9 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.4.5/maven-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.4.5/maven-plugin/reference/html/#build-image) + diff --git a/java/consent/pom.xml b/java/consent/pom.xml new file mode 100644 index 000000000..5bb59bb04 --- /dev/null +++ b/java/consent/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.6 + + + dev.sunbirdrc + consent + 0.0.1-SNAPSHOT + consent + Demo project for Spring Boot + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + io.jsonwebtoken + jjwt + 0.9.1 + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.bouncycastle + bcprov-jdk15on + 1.70 + + + in.divoc.api + keycloak-mobile-number-login-spi + + + in.divoc.api + keycloak-mobile-number-login-spi + 1.70 + + + dev.sunbirdrc + pojos + 2.0.3 + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/ConsentApplication.java b/java/consent/src/main/java/dev/sunbirdrc/consent/ConsentApplication.java new file mode 100644 index 000000000..19e07a263 --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/ConsentApplication.java @@ -0,0 +1,13 @@ +package dev.sunbirdrc.consent; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ConsentApplication { + + public static void main(String[] args) { + SpringApplication.run(ConsentApplication.class, args); + } + +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/constants/ConstentStatus.java b/java/consent/src/main/java/dev/sunbirdrc/consent/constants/ConstentStatus.java new file mode 100644 index 000000000..cdfc033c6 --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/constants/ConstentStatus.java @@ -0,0 +1,5 @@ +package dev.sunbirdrc.consent.constants; + +public enum ConstentStatus { + GRANTED, DENIED +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java new file mode 100644 index 000000000..cfcfbf12d --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java @@ -0,0 +1,44 @@ +package dev.sunbirdrc.consent.controller; + +import dev.sunbirdrc.consent.entity.Consent; +import dev.sunbirdrc.consent.exceptions.ConsentDefinitionNotFoundException; +import dev.sunbirdrc.consent.service.ConsentService; +import dev.sunbirdrc.pojos.dto.ConsentDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@Controller +public class ConsentController { + + private static final String STATUS = "status"; + @Autowired + private ConsentService consentService; + + @PostMapping("/api/v1/consent") + public ResponseEntity createConsent(@RequestBody ConsentDTO consentDTO) { + Consent consent = Consent.fromDTO(consentDTO); + Consent savedConsent = consentService.saveConsent(consent); + return new ResponseEntity<>(savedConsent, HttpStatus.CREATED); + } + + @GetMapping(value = "/api/v1/consent/{id}") + public ResponseEntity getConsentById(@PathVariable String id) throws ConsentDefinitionNotFoundException { + Consent consent = consentService.retrieveConsents(id); + return new ResponseEntity<>(consent, HttpStatus.OK); + } + + @PutMapping(value = "/api/v1/consent/{id}") + public ResponseEntity grantOrDenyConsent(@PathVariable String id, @RequestBody Map statusMap) { + try { + Consent consent = consentService.grantOrDenyConsent(statusMap.get(STATUS), id); + return new ResponseEntity<>(consent, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java b/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java new file mode 100644 index 000000000..b041472cb --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java @@ -0,0 +1,75 @@ +package dev.sunbirdrc.consent.entity; + +import dev.sunbirdrc.pojos.dto.ConsentDTO; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; +import java.util.Map; +import java.util.stream.Collectors; + +@Entity +@Table(name="consent") +@Getter +@Setter +public class Consent { + public static final String TABLE_NAME= "claims"; + public static final String CREATED_AT = "created_at"; + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator( + name = "UUID", + strategy = "org.hibernate.id.UUIDGenerator" + ) + @Column(updatable = false, nullable = false) + private String id; + @Column + private String entityName; + + @Column + private String osOwner; + @Column + private String entityId; + @Column(name = Consent.CREATED_AT) + private Date createdAt; + private Date updatedAt; + @Column + private boolean status; + @Column + private String requestorName; + @Column + private String requestorId; + @Column + private String expirationTime; + @ElementCollection + @MapKeyColumn(name="name") + @Column(name="value") + @CollectionTable(name="consent_fields", joinColumns=@JoinColumn(name="consent_id")) + private Map consentFields; + + @PrePersist + protected void onCreate() { + createdAt = new Date(); + updatedAt = new Date(); + status = false; + } + + @PreUpdate + protected void onUpdate() { + updatedAt = new Date(); + } + + public static Consent fromDTO(ConsentDTO consentDTO) { + Consent consent = new Consent(); + consent.entityName = consentDTO.getEntityName(); + consent.entityId = consentDTO.getEntityId(); + consent.requestorName = consentDTO.getRequestorName(); + consent.requestorId = consentDTO.getRequestorId(); + consent.consentFields = consentDTO.getConsentFieldsPath(); + consent.expirationTime = consentDTO.getConsentExpiryTime(); + consent.osOwner = consentDTO.getOsOwner().stream().collect(Collectors.joining(",")); + return consent; + } +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentDefinitionNotFoundException.java b/java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentDefinitionNotFoundException.java new file mode 100644 index 000000000..896fe007b --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentDefinitionNotFoundException.java @@ -0,0 +1,7 @@ +package dev.sunbirdrc.consent.exceptions; + +public class ConsentDefinitionNotFoundException extends Exception{ + public ConsentDefinitionNotFoundException(String message) { + super(message); + } +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java b/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java new file mode 100644 index 000000000..c8611fc39 --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java @@ -0,0 +1,9 @@ +package dev.sunbirdrc.consent.repository; + +import dev.sunbirdrc.consent.entity.Consent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ConsentRepository extends JpaRepository { +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java new file mode 100644 index 000000000..e3a1ae05c --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java @@ -0,0 +1,37 @@ +package dev.sunbirdrc.consent.service; + +import dev.sunbirdrc.consent.entity.Consent; +import dev.sunbirdrc.consent.exceptions.ConsentDefinitionNotFoundException; +import dev.sunbirdrc.consent.repository.ConsentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +import static dev.sunbirdrc.consent.constants.ConstentStatus.GRANTED; + +@Service +public class ConsentService { + @Autowired + private ConsentRepository consentRepository; + + public Consent saveConsent(Consent consent) { + return consentRepository.save(consent); + } + + public Consent retrieveConsents(String id) throws ConsentDefinitionNotFoundException { + Consent consent = consentRepository.findById(id).get(); + if(consent == null) throw new ConsentDefinitionNotFoundException("Invalid ID of consent"); + return consent; + } + + public Consent grantOrDenyConsent(String status, String id) throws Exception{ + Optional optConsent = consentRepository.findById(id); + Consent consent = optConsent.map(consent1 -> { + consent1.setStatus(status.equals(GRANTED.name())); + return consent1; + }).orElse(null); + if(consent == null) throw new ConsentDefinitionNotFoundException("Invalid ID of consent"); + return consentRepository.save(consent); + } +} diff --git a/java/consent/src/main/resources/application.properties b/java/consent/src/main/resources/application.properties new file mode 100644 index 000000000..82f948198 --- /dev/null +++ b/java/consent/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port=8083 + +spring.datasource.url=${connectionInfo_uri:jdbc:postgresql://0.0.0.0:5432/registry} +spring.datasource.username=${connectionInfo_username:postgres} +spring.datasource.password=${connectionInfo_password:postgres} + +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +sunbirdrc.url=${sunbirdrc_url:http://localhost:8081} diff --git a/java/consent/src/test/java/dev/sunbirdrc/consent/ConsentApplicationTests.java b/java/consent/src/test/java/dev/sunbirdrc/consent/ConsentApplicationTests.java new file mode 100644 index 000000000..501fa68e9 --- /dev/null +++ b/java/consent/src/test/java/dev/sunbirdrc/consent/ConsentApplicationTests.java @@ -0,0 +1,13 @@ +package dev.sunbirdrc.consent; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ConsentApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/java/pojos/src/main/java/dev/sunbirdrc/pojos/PluginRequestMessage.java b/java/pojos/src/main/java/dev/sunbirdrc/pojos/PluginRequestMessage.java index f229d3167..3ddb77dba 100644 --- a/java/pojos/src/main/java/dev/sunbirdrc/pojos/PluginRequestMessage.java +++ b/java/pojos/src/main/java/dev/sunbirdrc/pojos/PluginRequestMessage.java @@ -31,6 +31,11 @@ public class PluginRequestMessage { private String attestorSignin; private String conditions; private String status; + private String consentEntityName; + private String consentEntityId; + private List consentEntityOsOwner; + private Map consentFieldPath; + private String expirationTime; @Nullable private String userId; private Map> propertiesOSID; diff --git a/java/pojos/src/main/java/dev/sunbirdrc/pojos/dto/ConsentDTO.java b/java/pojos/src/main/java/dev/sunbirdrc/pojos/dto/ConsentDTO.java new file mode 100644 index 000000000..295fc3c0b --- /dev/null +++ b/java/pojos/src/main/java/dev/sunbirdrc/pojos/dto/ConsentDTO.java @@ -0,0 +1,16 @@ +package dev.sunbirdrc.pojos.dto; + +import lombok.Data; +import java.util.List; +import java.util.Map; + +@Data +public class ConsentDTO { + private String entityName; + private String entityId; + private String requestorName; + private String requestorId; + private Map consentFieldsPath; + private String consentExpiryTime; + private List osOwner; +} diff --git a/java/pom.xml b/java/pom.xml index 73c2c8b5b..2d105b979 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -41,7 +41,8 @@ claim apitest plugins - + consent + diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java index 3ccda1167..ec4f405a8 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java @@ -63,6 +63,14 @@ ResponseEntity internalErrorResponse(ResponseParams responseParams, Resp return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } + ResponseEntity forbiddenExceptionResponse(Exception e) { + ResponseParams responseParams = new ResponseParams(); + Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); + responseParams.setErrmsg(e.getMessage()); + responseParams.setStatus(Response.Status.UNSUCCESSFUL); + return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); + } + ResponseEntity createUnauthorizedExceptionResponse(Exception e) { ResponseParams responseParams = new ResponseParams(); Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java new file mode 100644 index 000000000..65dc04a5e --- /dev/null +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java @@ -0,0 +1,176 @@ +package dev.sunbirdrc.registry.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.sunbirdrc.pojos.Response; +import dev.sunbirdrc.pojos.ResponseParams; +import dev.sunbirdrc.registry.exception.ConsentForbiddenException; +import dev.sunbirdrc.registry.exception.RecordNotFoundException; +import dev.sunbirdrc.registry.helper.RegistryHelper; +import dev.sunbirdrc.registry.util.ConsentRequestClient; +import org.jetbrains.annotations.Nullable; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.*; + +@RestController +public class RegistryConsentController extends AbstractController { + + @Autowired + private ConsentRequestClient consentRequestClient; + @Autowired + private RegistryHelper registryHelper; + @Autowired + private ObjectMapper objectMapper; + private static Logger logger = LoggerFactory.getLogger(RegistryConsentController.class); + + private ArrayList getConsentFields(HttpServletRequest request) { + ArrayList fields = new ArrayList<>(); + KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal(); + try { + Map otherClaims = ((KeycloakPrincipal) principal.getPrincipal()).getKeycloakSecurityContext().getToken().getOtherClaims(); + if (otherClaims.keySet().contains(dev.sunbirdrc.registry.Constants.KEY_CONSENT) && otherClaims.get(dev.sunbirdrc.registry.Constants.KEY_CONSENT) instanceof Map) { + Map consentFields = (Map) otherClaims.get(dev.sunbirdrc.registry.Constants.KEY_CONSENT); + for (Object key : consentFields.keySet()) { + fields.add(key.toString()); + } + } + } catch (Exception ex) { + logger.error("Error while extracting other claims", ex); + } + return fields; + } + + @Nullable + private ResponseEntity getJsonNodeResponseEntity(JsonNode userInfoFromRegistry, String entityName, ArrayList consentFields) { + JsonNode jsonNode = userInfoFromRegistry.get(entityName); + if (jsonNode instanceof ArrayNode) { + ArrayNode values = (ArrayNode) jsonNode; + if (values.size() > 0) { + JsonNode node = values.get(0); + if (node instanceof ObjectNode) { + ObjectNode entityNode = copyWhiteListedFields(consentFields, node); + return new ResponseEntity<>(entityNode, HttpStatus.OK); + } + } + } + return null; + } + + private boolean isConsentTimeExpired(String createdAt, String expirationTime) { + OffsetDateTime currentDateTime = OffsetDateTime.now( ZoneOffset.UTC ); + OffsetDateTime odt = OffsetDateTime.parse(createdAt); + odt = odt.plus(Long.parseLong(expirationTime), ChronoUnit.SECONDS); + return odt.compareTo(currentDateTime) < 0; + } + private ObjectNode copyWhiteListedFields(ArrayList fields, JsonNode dataNode) { + ObjectNode node = JsonNodeFactory.instance.objectNode(); + for (String key : fields) { + node.set(key, dataNode.get(key)); + } + return node; + } + + @PostMapping(value = "/api/v1/consent") + public ResponseEntity createConsent(@RequestBody ObjectNode objectNode, HttpServletRequest request) throws Exception { + ResponseParams responseParams = new ResponseParams(); + consentRequestClient.addConsent(objectNode, request); + responseParams.setStatus(Response.Status.SUCCESSFUL); + return new ResponseEntity<>(responseParams, HttpStatus.OK); + } + + @GetMapping("/api/v1/consent/{consentId}") + public ResponseEntity getConsent(@PathVariable String consentId, HttpServletRequest request) throws Exception { + JsonNode consent = null; + ResponseParams responseParams = new ResponseParams(); + Response response = new Response(Response.API_ID.READ, "OK", responseParams); + try { + consent = consentRequestClient.getConsent(consentId); + } catch (Exception exception) { + return internalErrorResponse(responseParams, response, exception); + } + boolean status = consent.get("status").asText().equals("true"); + String keycloakUserId = registryHelper.getKeycloakUserId(request); + boolean isOwner = keycloakUserId.equals(consent.get("requestorId").asText()); + try { + if(isConsentTimeExpired(consent.get("createdAt").asText(), consent.get("expirationTime").asText())) { + final String consentTimeExpired = "Consent Time Expired"; + throw new ConsentForbiddenException(consentTimeExpired); + } + if(!isOwner) { + final String forbidden = "You are not authorized to access this consent"; + throw new ConsentForbiddenException(forbidden); + } + } catch (ConsentForbiddenException consentForbiddenException) { + return forbiddenExceptionResponse(consentForbiddenException); + } + if(status) { + JsonNode userInfoFromRegistry = null; + String[] osOwners = consent.get("osOwner").asText().split(","); + String entityName = consent.get("entityName").asText(); + for(String owner : osOwners) { + userInfoFromRegistry = consentRequestClient.searchUser(entityName, owner); + if (userInfoFromRegistry != null) + break; + } + ArrayList consentFields = new ArrayList<>(objectMapper.convertValue(consent.get("consentFields"), Map.class).keySet()); + ResponseEntity entityNode = getJsonNodeResponseEntity(userInfoFromRegistry, entityName, consentFields); + if (entityNode != null) return entityNode; + } + final String consentNotGranted = "Consent is rejected or not granted until now"; + return new ResponseEntity<>(consentNotGranted, HttpStatus.OK); + } + + @RequestMapping(value = "/partner/api/v1/{entityName}", method = RequestMethod.GET) + public ResponseEntity getEntityWithConsent( + @PathVariable String entityName, + HttpServletRequest request) { + try { + ArrayList fields = getConsentFields(request); + JsonNode userInfoFromRegistry = registryHelper.getRequestedUserDetails(request, entityName); + ResponseEntity responseEntity = getJsonNodeResponseEntity(userInfoFromRegistry, entityName, fields); + if(responseEntity != null) + return responseEntity; + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } catch (RecordNotFoundException ex) { + logger.error("Error in finding the entity", ex); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } catch (Exception e) { + logger.error("Error in partner api access", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping("/api/v1/consent/{consentId}") + public ResponseEntity grantOrRejectClaim(@PathVariable String consentId, @RequestBody JsonNode jsonNode, HttpServletRequest request) throws Exception { + ResponseParams responseParams = new ResponseParams(); + Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); + JsonNode consent; + try { + consent = consentRequestClient.getConsent(consentId); + } catch (Exception e) { + return internalErrorResponse(responseParams, response, e); + } + String userId = registryHelper.getUserId(request, consent.get("entityName").asText()); + String[] osOwners = consent.get("osOwner").asText().split(","); + boolean isOwner = Arrays.stream(osOwners).filter(owner -> owner.equals(userId)) != null; + if(isOwner) { + return consentRequestClient.grantOrRejectClaim(consentId, jsonNode); + } + return forbiddenExceptionResponse(new ConsentForbiddenException("You are not authorized to update this consent")); + } +} diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java index a41b6d4fd..c7e7a43cd 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java @@ -6,10 +6,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import dev.sunbirdrc.keycloak.OwnerCreationException; -import dev.sunbirdrc.pojos.AsyncRequest; -import dev.sunbirdrc.pojos.PluginResponseMessage; -import dev.sunbirdrc.pojos.Response; -import dev.sunbirdrc.pojos.ResponseParams; +import dev.sunbirdrc.pojos.*; import dev.sunbirdrc.registry.dao.NotFoundException; import dev.sunbirdrc.registry.entities.AttestationPolicy; import dev.sunbirdrc.registry.exception.AttestationNotFoundException; @@ -27,8 +24,6 @@ import dev.sunbirdrc.validators.ValidationException; import org.apache.commons.lang3.StringUtils; import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -61,8 +56,6 @@ public class RegistryEntityController extends AbstractController { @Autowired private AsyncRequest asyncRequest; - - @Value("${authentication.enabled:true}") boolean securityEnabled; @Value("${certificate.enableExternalTemplates:false}") boolean externalTemplatesEnabled; @@ -370,59 +363,6 @@ private JsonNode getAttestationNode(String attestationId, JsonNode node) throws return attestationNode; } - @RequestMapping(value = "/partner/api/v1/{entityName}", method = RequestMethod.GET) - public ResponseEntity getEntityWithConsent( - @PathVariable String entityName, - HttpServletRequest request) { - try { - ArrayList fields = getConsentFields(request); - JsonNode userInfoFromRegistry = registryHelper.getRequestedUserDetails(request, entityName); - JsonNode jsonNode = userInfoFromRegistry.get(entityName); - if (jsonNode instanceof ArrayNode) { - ArrayNode values = (ArrayNode) jsonNode; - if (values.size() > 0) { - JsonNode node = values.get(0); - if (node instanceof ObjectNode) { - ObjectNode entityNode = copyWhiteListedFields(fields, node); - return new ResponseEntity<>(entityNode, HttpStatus.OK); - } - } - } - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } catch (RecordNotFoundException ex) { - logger.error("Error in finding the entity", ex); - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } catch (Exception e) { - logger.error("Error in partner api access", e); - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } - } - - private ObjectNode copyWhiteListedFields(ArrayList fields, JsonNode dataNode) { - ObjectNode node = JsonNodeFactory.instance.objectNode(); - for (String key : fields) { - node.set(key, dataNode.get(key)); - } - return node; - } - - private ArrayList getConsentFields(HttpServletRequest request) { - ArrayList fields = new ArrayList<>(); - KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal(); - try { - Map otherClaims = ((KeycloakPrincipal) principal.getPrincipal()).getKeycloakSecurityContext().getToken().getOtherClaims(); - if (otherClaims.keySet().contains(dev.sunbirdrc.registry.Constants.KEY_CONSENT) && otherClaims.get(dev.sunbirdrc.registry.Constants.KEY_CONSENT) instanceof Map) { - Map consentFields = (Map) otherClaims.get(dev.sunbirdrc.registry.Constants.KEY_CONSENT); - for (Object key : consentFields.keySet()) { - fields.add(key.toString()); - } - } - } catch (Exception ex) { - logger.error("Error while extracting other claims", ex); - } - return fields; - } - @RequestMapping(value = "/api/v1/{entityName}/{entityId}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_PDF_VALUE, MediaType.TEXT_HTML_VALUE, Constants.SVG_MEDIA_TYPE}) public ResponseEntity getEntityType(@PathVariable String entityName, diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/exception/ConsentForbiddenException.java b/java/registry/src/main/java/dev/sunbirdrc/registry/exception/ConsentForbiddenException.java new file mode 100644 index 000000000..a90e69a75 --- /dev/null +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/exception/ConsentForbiddenException.java @@ -0,0 +1,7 @@ +package dev.sunbirdrc.registry.exception; + +public class ConsentForbiddenException extends Exception{ + public ConsentForbiddenException(String message) { + super(message); + } +} diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java new file mode 100644 index 000000000..eb1828617 --- /dev/null +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java @@ -0,0 +1,82 @@ +package dev.sunbirdrc.registry.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.sunbirdrc.actors.factory.PluginRouter; +import dev.sunbirdrc.pojos.PluginRequestMessage; +import dev.sunbirdrc.registry.helper.RegistryHelper; +import dev.sunbirdrc.registry.middleware.util.OSSystemFields; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +import static dev.sunbirdrc.registry.middleware.util.Constants.ENTITY_TYPE; +import static dev.sunbirdrc.registry.middleware.util.Constants.FILTERS; + +@Component +public class ConsentRequestClient { + + private final String consentUrl; + private final RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private RegistryHelper registryHelper; + + public ConsentRequestClient(@Value("${consent.url}") String consentUrl, RestTemplate restTemplate) { + this.consentUrl = consentUrl; + this.restTemplate = restTemplate; + } + + public JsonNode getConsent(String consentId) throws Exception{ + return restTemplate.getForObject( + consentUrl + "/api/v1/consent/" + consentId, + JsonNode.class + ); + } + + public JsonNode searchUser(String entityName, String userId) throws Exception { + ObjectNode payload = JsonNodeFactory.instance.objectNode(); + payload.set(ENTITY_TYPE, JsonNodeFactory.instance.arrayNode().add(entityName)); + ObjectNode filters = JsonNodeFactory.instance.objectNode(); + filters.set(OSSystemFields.osOwner.toString(), JsonNodeFactory.instance.objectNode().put("contains", userId)); + payload.set(FILTERS, filters); + return registryHelper.searchEntity(payload); + } + + public void addConsent(ObjectNode objectNode, HttpServletRequest request) throws Exception { + final String attestorPlugin = "did:internal:ConsentPluginActor"; + PluginRequestMessage pluginRequestMessage = PluginRequestMessage.builder().build(); + pluginRequestMessage.setAttestorPlugin(attestorPlugin); + pluginRequestMessage.setConsentEntityName(objectNode.get("entityName").asText()); + pluginRequestMessage.setConsentEntityId(objectNode.get("entityId").asText()); + pluginRequestMessage.setExpirationTime(objectNode.get("consentExpiryTime").asText()); + pluginRequestMessage.setSourceEntity(objectNode.get("requestorName").asText()); + pluginRequestMessage.setUserId(registryHelper.getKeycloakUserId(request)); + pluginRequestMessage.setStatus(objectNode.get("status").asText()); + pluginRequestMessage.setConsentFieldPath(objectMapper.convertValue(objectNode.get("consentFieldsPath"), Map.class)); + pluginRequestMessage.setConsentEntityOsOwner(objectMapper.convertValue(objectNode.get("osOwner"), List.class)); + PluginRouter.route(pluginRequestMessage); + } + + public ResponseEntity grantOrRejectClaim(String consentId, JsonNode jsonNode) throws Exception { + final String attestorPlugin = "did:internal:ConsentPluginActor"; + PluginRequestMessage pluginRequestMessage = PluginRequestMessage.builder().build(); + pluginRequestMessage.setAttestorPlugin(attestorPlugin); + pluginRequestMessage.setStatus(jsonNode.get("status").asText()); + String consent = "{\"consentId\" : " + "\"" + consentId + "\"" + "}"; + JsonNode additionalInput = objectMapper.readValue(consent, JsonNode.class); + pluginRequestMessage.setAdditionalInputs(additionalInput); + PluginRouter.route(pluginRequestMessage); + return null; + } +} diff --git a/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java b/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java new file mode 100644 index 000000000..fc95e25bb --- /dev/null +++ b/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java @@ -0,0 +1,52 @@ +package dev.sunbirdrc.actors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import dev.sunbirdrc.pojos.PluginRequestMessage; +import dev.sunbirdrc.pojos.dto.ConsentDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.web.client.RestTemplate; +import org.sunbird.akka.core.BaseActor; +import org.sunbird.akka.core.MessageProtos; + +import java.io.IOException; + +public class ConsentPluginActor extends BaseActor { + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private RestTemplate restTemplate = new RestTemplate(); + private final String consentUrl = System.getenv().getOrDefault("consent_url", "http://localhost:8083"); + @Override + protected void onReceive(MessageProtos.Message request) throws Throwable { + PluginRequestMessage pluginRequestMessage = new ObjectMapper().readValue(request.getPayload().getStringValue(), PluginRequestMessage.class); + if(pluginRequestMessage.getUserId() == null) { + grantOrRejectConsent(pluginRequestMessage); + return; + } + createConsent(pluginRequestMessage); + } + + private void grantOrRejectConsent(PluginRequestMessage pluginRequestMessage) throws IOException { + String requestBody = "{\"status\": " + "\"" + pluginRequestMessage.getStatus() + "\"" + "}"; + String consentId = pluginRequestMessage.getAdditionalInputs().get("consentId").asText(); + JsonNode jsonNode = new ObjectMapper().readValue(requestBody, JsonNode.class); + restTemplate.exchange(consentUrl + "/api/v1/consent/" + consentId, HttpMethod.PUT,new HttpEntity<>(jsonNode), Object.class); + } + + private void createConsent(PluginRequestMessage pluginRequestMessage) { + ConsentDTO consentDTO = new ConsentDTO(); + consentDTO.setEntityId(pluginRequestMessage.getConsentEntityId()); + consentDTO.setEntityName(pluginRequestMessage.getConsentEntityName()); + consentDTO.setOsOwner(pluginRequestMessage.getConsentEntityOsOwner()); + consentDTO.setConsentFieldsPath(pluginRequestMessage.getConsentFieldPath()); + consentDTO.setConsentExpiryTime(pluginRequestMessage.getExpirationTime()); + consentDTO.setRequestorName(pluginRequestMessage.getSourceEntity()); + consentDTO.setRequestorId(pluginRequestMessage.getUserId()); + restTemplate.exchange(consentUrl + "/api/v1/consent", HttpMethod.POST, new HttpEntity<>(consentDTO), Object.class); + } +} diff --git a/java/sunbirdrc-actors/src/main/resources/sunbirdrc-actors.conf b/java/sunbirdrc-actors/src/main/resources/sunbirdrc-actors.conf index 9114bb0ea..8aa19c1a9 100644 --- a/java/sunbirdrc-actors/src/main/resources/sunbirdrc-actors.conf +++ b/java/sunbirdrc-actors/src/main/resources/sunbirdrc-actors.conf @@ -80,6 +80,11 @@ nr-of-instances = 3 dispatcher = rr-dispatcher } + /ConsentPluginActor { + router = smallest-mailbox-pool + nr-of-instances = 3 + dispatcher = rr-dispatcher + } /GenericPluginActor { router = smallest-mailbox-pool nr-of-instances = 3 From 99cf2c965f4569d492f5106da3250764bea78781 Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Thu, 8 Dec 2022 09:20:49 +0530 Subject: [PATCH 2/9] Add Docker image creation files --- java/consent/Dockerfile | 4 ++++ java/consent/Makefile | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 java/consent/Dockerfile create mode 100644 java/consent/Makefile diff --git a/java/consent/Dockerfile b/java/consent/Dockerfile new file mode 100644 index 000000000..4f1f5af6e --- /dev/null +++ b/java/consent/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:8-jdk-alpine +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/java/consent/Makefile b/java/consent/Makefile new file mode 100644 index 000000000..e4a6c3ce1 --- /dev/null +++ b/java/consent/Makefile @@ -0,0 +1,9 @@ +rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2)) +SOURCES := $(call rwildcard,java/,*.java) +build: target/consent-0.0.1-SNAPSHOT.jar + echo ${SOURCES} + cd target && docker build -t dockerhub/sunbird-rc-consent-ms .. + +target/claim-0.0.1-SNAPSHOT.jar: $(SOURCES) + echo $(SOURCES) + ../mvnw -DskipTests clean install \ No newline at end of file From 70b0efb10d4721be35736f68dbbf46e0924cb963 Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Thu, 8 Dec 2022 15:48:21 +0530 Subject: [PATCH 3/9] add unit test cases and one e2e test case --- .../java/e2e/registry/ConsentRequest.json | 13 +++ .../test/java/e2e/registry/PlaceRequest.json | 8 ++ .../java/e2e/registry/PlaceSchemaRequest.json | 6 ++ java/consent/pom.xml | 21 +++-- .../consent/service/ConsentService.java | 3 +- .../consent/service/ConsentServiceTest.java | 86 +++++++++++++++++++ java/registry/pom.xml | 4 + 7 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 java/apitest/src/test/java/e2e/registry/ConsentRequest.json create mode 100644 java/apitest/src/test/java/e2e/registry/PlaceRequest.json create mode 100644 java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json create mode 100644 java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java diff --git a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json new file mode 100644 index 000000000..6e841df15 --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json @@ -0,0 +1,13 @@ +{ + "entityName": "Place", + "entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d", + "status": "false", + "requestorName": "Random", + "requestorId": "1", + "consentFieldsPath": { + "name": "$.name", + "country": "" + }, + "osOwner": ["c8211527-9d2f-4e08-8d8a-b57ef8be6eba","anonymous"], + "consentExpiryTime": "1000" +} \ No newline at end of file diff --git a/java/apitest/src/test/java/e2e/registry/PlaceRequest.json b/java/apitest/src/test/java/e2e/registry/PlaceRequest.json new file mode 100644 index 000000000..05639c69d --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/PlaceRequest.json @@ -0,0 +1,8 @@ +{ + "name": "Aurangabad", + "city": "Aurangabad", + "country": "India", + "email": "120@mail.com", + "contact": "1234567890", + "addressRegion": "Marathwada" +} \ No newline at end of file diff --git a/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json b/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json new file mode 100644 index 000000000..4961e518c --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json @@ -0,0 +1,6 @@ +{ + "name": "place", + "schema": "{\"$schema\": \"http://json-schema.org/draft-07/schema\", \"type\": \"object\", \"properties\": { \"Place\": { \"$ref\": \"#/definitions/Place\" } }, \"required\": [ \"Place\" ], \"title\": \"Place\", \"definitions\": { \"Place\": { \"$id\": \"#/properties/Place\", \"type\": \"object\", \"title\": \"The Place Schema\", \"required\": [ \"name\", \"city\", \"addressRegion\", \"country\" ], \"properties\": { \"name\": { \"type\": \"string\" }, \"city\": { \"type\": \"string\" }, \"addressLocality\": { \"type\": \"string\" }, \"addressRegion\": { \"type\": \"string\" }, \"country\": { \"type\": \"string\" }, \"postalCode\": { \"type\": \"string\" }, \"contact\": { \"type\": \"string\" }, \"email\": { \"type\": \"string\" } } } }, \"_osConfig\": { \"privateFields\": [ \"name\" ], \"signedFields\": [ \"country\" ], \"roles\": [ ], \"inviteRoles\": [ \"anonymous\"], \"ownershipAttributes\": [], \"attestationPolicies\": [ { \"name\": \"schemaAttestation\", \"conditions\": \"(ATTESTOR#$.[*]#.contains('board-cbse'))\", \"type\": \"AUTOMATED\", \"attestorPlugin\": \"did:internal:ClaimPluginActor?entity=board-cbse\", \"attestationProperties\": { \"country\": \"$.country\", \"contact\": \"$.contact\" } } ], \"credentialTemplate\": { \"@context\": [ \"https://www.w3.org/2018/credentials/v1\", \"https://gist.githubusercontent.com/varadeth/c781559f8d3954fda040d1be0fb2187d/raw/7e951447b3aaf670d407068274fe3ace814c55a4/gistfile1.json\" ], \"type\": [ \"VerifiableCredential\", \"AttestationCertificate\" ], \"issuer\": \"http://www.india.gov.in\", \"issuanceDate\": \"2022-08-08T12:00:00Z\", \"credentialSubject\": { \"type\": \"Place\", \"name\": \"{{name}}\", \"country\": \"{{country}}\" }, \"evidence\": { \"type\": \"Affiliation\", \"postalCode\": \"{{postalCode}}\", \"contact\": \"{{contact}}\" } } }}", + "status": "PUBLISHED" + +} \ No newline at end of file diff --git a/java/consent/pom.xml b/java/consent/pom.xml index 5bb59bb04..9abee7c09 100644 --- a/java/consent/pom.xml +++ b/java/consent/pom.xml @@ -55,21 +55,24 @@ bcprov-jdk15on 1.70 - - in.divoc.api - keycloak-mobile-number-login-spi - - - in.divoc.api - keycloak-mobile-number-login-spi - 1.70 - dev.sunbirdrc pojos 2.0.3 compile + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.12.0 + test + diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java index e3a1ae05c..b0721745e 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java @@ -20,8 +20,7 @@ public Consent saveConsent(Consent consent) { } public Consent retrieveConsents(String id) throws ConsentDefinitionNotFoundException { - Consent consent = consentRepository.findById(id).get(); - if(consent == null) throw new ConsentDefinitionNotFoundException("Invalid ID of consent"); + Consent consent = consentRepository.findById(id).orElseThrow(() -> new ConsentDefinitionNotFoundException("Invalid ID of consent")); return consent; } diff --git a/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java new file mode 100644 index 000000000..7776ff053 --- /dev/null +++ b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java @@ -0,0 +1,86 @@ +package dev.sunbirdrc.consent.service; + +import dev.sunbirdrc.consent.entity.Consent; +import dev.sunbirdrc.consent.exceptions.ConsentDefinitionNotFoundException; +import dev.sunbirdrc.consent.repository.ConsentRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.HashMap; +import java.util.Optional; + +import static dev.sunbirdrc.consent.constants.ConstentStatus.GRANTED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class ConsentServiceTest { + @Mock + private ConsentRepository consentRepository; + @InjectMocks + private ConsentService consentService; + + @Test + public void shouldCallSaveMethodInConsentRepository() { + Consent consent = new Consent(); + consent.setEntityName("Teacher"); + consent.setEntityId("123"); + consent.setRequestorName("Institute"); + consent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + consent.setConsentFields(map); + consent.setExpirationTime("1000"); + consent.setOsOwner("789"); + consentService.saveConsent(consent); + verify(consentRepository, times(1)).save(consent); + } + + @Test + public void shouldRetrieveConsentsBasedOnId() throws ConsentDefinitionNotFoundException { + Consent expectedConsent = new Consent(); + expectedConsent.setEntityName("Teacher"); + expectedConsent.setEntityId("123"); + expectedConsent.setRequestorName("Institute"); + expectedConsent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + expectedConsent.setConsentFields(map); + expectedConsent.setExpirationTime("1000"); + expectedConsent.setOsOwner("789"); + when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); + Consent actualConsent = consentService.retrieveConsents("123"); + verify(consentRepository, times(1)).findById("123"); + assertEquals(expectedConsent, actualConsent); + } + + @Test + public void shouldThrowExceptionIfConsentIsNotAvailableForId() { + when(consentRepository.findById("123")).thenReturn(Optional.ofNullable(null)); + assertThrows(ConsentDefinitionNotFoundException.class, () -> consentService.retrieveConsents("123")); + } + + @Test + public void shouldGrantConsent() throws Exception { + Consent consent = new Consent(); + consent.setEntityName("Teacher"); + consent.setEntityId("123"); + consent.setRequestorName("Institute"); + consent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + consent.setConsentFields(map); + consent.setExpirationTime("1000"); + consent.setOsOwner("789"); + when(consentRepository.findById("123")).thenReturn(Optional.of(consent)); + consentService.grantOrDenyConsent(GRANTED.name(), "123"); + consent.setStatus(true); + verify(consentRepository, times(1)).findById("123"); + verify(consentRepository, times(1)).save(consent); + } +} diff --git a/java/registry/pom.xml b/java/registry/pom.xml index 46fb30968..d5bd89afc 100644 --- a/java/registry/pom.xml +++ b/java/registry/pom.xml @@ -69,6 +69,10 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.retry spring-retry From 23b5b987c82b4cb3d8e671d1cfebc408addf2f5a Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Thu, 8 Dec 2022 19:29:34 +0530 Subject: [PATCH 4/9] add api to get consent based on whom consent is raised and added unit and e2e tests --- docker-compose.yml | 15 +++ .../e2e/registry/GrantConsentRequest.json | 3 + .../java/e2e/registry/PlaceSchemaRequest.json | 2 +- .../test/java/e2e/registry/registry.feature | 97 +++++++++++++++++++ .../consent/controller/ConsentController.java | 7 ++ .../dev/sunbirdrc/consent/entity/Consent.java | 8 +- .../consent/repository/ConsentRepository.java | 3 + .../consent/service/ConsentService.java | 5 + .../consent/service/ConsentServiceTest.java | 36 ++++++- .../controller/RegistryConsentController.java | 7 ++ .../registry/util/ConsentRequestClient.java | 4 + .../src/main/resources/application.yml | 2 + 12 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 java/apitest/src/test/java/e2e/registry/GrantConsentRequest.json diff --git a/docker-compose.yml b/docker-compose.yml index 67daf77d6..e6aa60dd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,7 @@ services: - sunbird_sso_client_id=${KEYCLOAK_CLIENT_ID-registry-frontend} - sunbird_sso_admin_client_secret=${KEYCLOAK_SECRET} - claims_url=http://claim-ms:8082 + - consent_url=http://consent-ms:8083 - sign_url=http://certificate-signer:8079/sign - sign_health_check_url=http://certificate-signer:8079/health - signature_enabled=true @@ -127,6 +128,20 @@ services: interval: 30s timeout: 10s retries: 4 + consent-ms: + image: dockerhub/sunbird-rc-consent-ms:${RELEASE_VERSION} + environment: + - connectionInfo_uri=jdbc:postgresql://db:5432/registry + - connectionInfo_username=postgres + - connectionInfo_password=postgres + - sunbirdrc_url=http://registry:8081 + ports: + - "8083:8083" + depends_on: + db: + condition: service_started + registry: + condition: service_started certificate-signer: image: dockerhub/sunbird-rc-certificate-signer:${RELEASE_VERSION} environment: diff --git a/java/apitest/src/test/java/e2e/registry/GrantConsentRequest.json b/java/apitest/src/test/java/e2e/registry/GrantConsentRequest.json new file mode 100644 index 000000000..3f36ce1ab --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/GrantConsentRequest.json @@ -0,0 +1,3 @@ +{ + "status": "GRANTED" +} \ No newline at end of file diff --git a/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json b/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json index 4961e518c..59ed95504 100644 --- a/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json +++ b/java/apitest/src/test/java/e2e/registry/PlaceSchemaRequest.json @@ -1,6 +1,6 @@ { "name": "place", - "schema": "{\"$schema\": \"http://json-schema.org/draft-07/schema\", \"type\": \"object\", \"properties\": { \"Place\": { \"$ref\": \"#/definitions/Place\" } }, \"required\": [ \"Place\" ], \"title\": \"Place\", \"definitions\": { \"Place\": { \"$id\": \"#/properties/Place\", \"type\": \"object\", \"title\": \"The Place Schema\", \"required\": [ \"name\", \"city\", \"addressRegion\", \"country\" ], \"properties\": { \"name\": { \"type\": \"string\" }, \"city\": { \"type\": \"string\" }, \"addressLocality\": { \"type\": \"string\" }, \"addressRegion\": { \"type\": \"string\" }, \"country\": { \"type\": \"string\" }, \"postalCode\": { \"type\": \"string\" }, \"contact\": { \"type\": \"string\" }, \"email\": { \"type\": \"string\" } } } }, \"_osConfig\": { \"privateFields\": [ \"name\" ], \"signedFields\": [ \"country\" ], \"roles\": [ ], \"inviteRoles\": [ \"anonymous\"], \"ownershipAttributes\": [], \"attestationPolicies\": [ { \"name\": \"schemaAttestation\", \"conditions\": \"(ATTESTOR#$.[*]#.contains('board-cbse'))\", \"type\": \"AUTOMATED\", \"attestorPlugin\": \"did:internal:ClaimPluginActor?entity=board-cbse\", \"attestationProperties\": { \"country\": \"$.country\", \"contact\": \"$.contact\" } } ], \"credentialTemplate\": { \"@context\": [ \"https://www.w3.org/2018/credentials/v1\", \"https://gist.githubusercontent.com/varadeth/c781559f8d3954fda040d1be0fb2187d/raw/7e951447b3aaf670d407068274fe3ace814c55a4/gistfile1.json\" ], \"type\": [ \"VerifiableCredential\", \"AttestationCertificate\" ], \"issuer\": \"http://www.india.gov.in\", \"issuanceDate\": \"2022-08-08T12:00:00Z\", \"credentialSubject\": { \"type\": \"Place\", \"name\": \"{{name}}\", \"country\": \"{{country}}\" }, \"evidence\": { \"type\": \"Affiliation\", \"postalCode\": \"{{postalCode}}\", \"contact\": \"{{contact}}\" } } }}", + "schema": "{\"$schema\": \"http://json-schema.org/draft-07/schema\", \"type\": \"object\", \"properties\": { \"Place\": { \"$ref\": \"#/definitions/Place\" } }, \"required\": [ \"Place\" ], \"title\": \"Place\", \"definitions\": { \"Place\": { \"$id\": \"#/properties/Place\", \"type\": \"object\", \"title\": \"The Place Schema\", \"required\": [ \"name\", \"city\", \"addressRegion\", \"country\" ], \"properties\": { \"name\": { \"type\": \"string\" }, \"city\": { \"type\": \"string\" }, \"addressLocality\": { \"type\": \"string\" }, \"addressRegion\": { \"type\": \"string\" }, \"country\": { \"type\": \"string\" }, \"postalCode\": { \"type\": \"string\" }, \"contact\": { \"type\": \"string\" }, \"email\": { \"type\": \"string\" } } } }, \"_osConfig\": { \"privateFields\": [ \"name\" ], \"signedFields\": [ \"country\" ], \"roles\": [ ], \"inviteRoles\": [ \"anonymous\"], \"ownershipAttributes\": [{\n \"email\": \"/email\",\n \"mobile\": \"/email\",\n \"userId\": \"/email\"\n }], \"attestationPolicies\": [ { \"name\": \"schemaAttestation\", \"conditions\": \"(ATTESTOR#$.[*]#.contains('board-cbse'))\", \"type\": \"AUTOMATED\", \"attestorPlugin\": \"did:internal:ClaimPluginActor?entity=board-cbse\", \"attestationProperties\": { \"country\": \"$.country\", \"contact\": \"$.contact\" } } ], \"credentialTemplate\": { \"@context\": [ \"https://www.w3.org/2018/credentials/v1\", \"https://gist.githubusercontent.com/varadeth/c781559f8d3954fda040d1be0fb2187d/raw/7e951447b3aaf670d407068274fe3ace814c55a4/gistfile1.json\" ], \"type\": [ \"VerifiableCredential\", \"AttestationCertificate\" ], \"issuer\": \"http://www.india.gov.in\", \"issuanceDate\": \"2022-08-08T12:00:00Z\", \"credentialSubject\": { \"type\": \"Place\", \"name\": \"{{name}}\", \"country\": \"{{country}}\" }, \"evidence\": { \"type\": \"Affiliation\", \"postalCode\": \"{{postalCode}}\", \"contact\": \"{{contact}}\" } } }}", "status": "PUBLISHED" } \ No newline at end of file diff --git a/java/apitest/src/test/java/e2e/registry/registry.feature b/java/apitest/src/test/java/e2e/registry/registry.feature index 51655f9dc..cc211ed2a 100644 --- a/java/apitest/src/test/java/e2e/registry/registry.feature +++ b/java/apitest/src/test/java/e2e/registry/registry.feature @@ -6,6 +6,8 @@ Feature: Registry api tests * url registryUrl * def admin_token = "" * def sleep = function(millis){ java.lang.Thread.sleep(millis) } + * def placeOsid = "" + * def placeOwner = "" Scenario: health check Given path 'health' @@ -183,3 +185,98 @@ Feature: Registry api tests And header Authorization = student_token When method get Then status 404 + + Scenario: Create consent and verify its apis + # get admin token + * url authUrl + * path 'auth/realms/sunbird-rc/protocol/openid-connect/token' + * header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8' + * header Host = 'keycloak:8080' + * form field grant_type = 'client_credentials' + * form field client_id = 'admin-api' + * form field client_secret = 'a52c5f4a-89fd-40b9-aea2-3f711f14c889' + * method post + Then status 200 + And print response.access_token + * def admin_token = 'Bearer ' + response.access_token + + # create place schema + Given url registryUrl + And path 'api/v1/Schema' + And header Authorization = admin_token + And request read('PlaceSchemaRequest.json') + When method post + Then status 200 + And response.params.status == "SUCCESSFUL" + + # create entity for place + Given url registryUrl + And path 'api/v1/Place' + And header Authorization = admin_token + * def placeRequest = read('PlaceRequest.json') + And request placeRequest + When method post + Then status 200 + And def placeOsid = response.result.Place.osid + + # fetch token for place entity's owners token + * url authUrl + * path 'auth/realms/sunbird-rc/protocol/openid-connect/token' + * header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8' + * header Host = 'keycloak:8080' + * form field grant_type = 'password' + * form field client_id = 'registry-frontend' + * form field username = placeRequest.email + * form field password = 'abcd@123' + * method post + Then status 200 + And print response.access_token + * def place_token = 'Bearer ' + response.access_token + * sleep(3000) + + # get entity by id + Given url registryUrl + And path 'api/v1/Place/' + placeOsid + And header Authorization = place_token + When method get + Then status 200 + And def placeOwner = response.osOwner + + # create consent for entity Place + Given url registryUrl + And path 'api/v1/consent' + And header Authorization = admin_token + * def consentRequest = read('ConsentRequest.json') + * consentRequest.entityId = placeOsid + * consentRequest.osOwner = placeOwner + And request consentRequest + When method post + Then status 200 + * sleep(3000) + + # fetch consent by owner + Given url registryUrl + And path 'api/v1/consent/' + And header Authorization = place_token + When method get + Then status 200 + And print response + And def consentId = response[0].id + And print consentId + + # grant consent + Given url registryUrl + And path 'api/v1/consent/' + consentId + And header Authorization = place_token + * def grantConsentRequest = read('GrantConsentRequest.json') + And request grantConsentRequest + When method put + Then status 200 + * sleep(3000) + + # fetch consent by id + Given url registryUrl + And path 'api/v1/consent/' + consentId + And header Authorization = admin_token + When method get + Then status 200 \ No newline at end of file diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java index cfcfbf12d..80b2940cc 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.Map; @Controller @@ -41,4 +42,10 @@ public ResponseEntity grantOrDenyConsent(@PathVariable String id, @Requ return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } + + @GetMapping(value = "/api/v1/consent/owner/{ownerId}") + public ResponseEntity> getConsentByOwnerId(@PathVariable String ownerId) { + List consent = consentService.retrieveConsentByOwnerId(ownerId); + return new ResponseEntity<>(consent, HttpStatus.OK); + } } diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java b/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java index b041472cb..c08d6422b 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java @@ -7,8 +7,8 @@ import javax.persistence.*; import java.util.Date; +import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Entity @Table(name="consent") @@ -28,8 +28,10 @@ public class Consent { @Column private String entityName; + @ElementCollection + @CollectionTable(name = "consent_owner_list", joinColumns = @JoinColumn(name = "id")) @Column - private String osOwner; + private List osOwner; @Column private String entityId; @Column(name = Consent.CREATED_AT) @@ -69,7 +71,7 @@ public static Consent fromDTO(ConsentDTO consentDTO) { consent.requestorId = consentDTO.getRequestorId(); consent.consentFields = consentDTO.getConsentFieldsPath(); consent.expirationTime = consentDTO.getConsentExpiryTime(); - consent.osOwner = consentDTO.getOsOwner().stream().collect(Collectors.joining(",")); + consent.osOwner = consentDTO.getOsOwner(); return consent; } } diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java b/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java index c8611fc39..d150fe389 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java @@ -4,6 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface ConsentRepository extends JpaRepository { + List findByOsOwner(String ownerId); } diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java index b0721745e..8bd6b6541 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Optional; import static dev.sunbirdrc.consent.constants.ConstentStatus.GRANTED; @@ -33,4 +34,8 @@ public Consent grantOrDenyConsent(String status, String id) throws Exception{ if(consent == null) throw new ConsentDefinitionNotFoundException("Invalid ID of consent"); return consentRepository.save(consent); } + + public List retrieveConsentByOwnerId(String ownerId) { + return consentRepository.findByOsOwner(ownerId); + } } diff --git a/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java index 7776ff053..3830c3321 100644 --- a/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java +++ b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java @@ -9,7 +9,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Optional; import static dev.sunbirdrc.consent.constants.ConstentStatus.GRANTED; @@ -28,6 +30,8 @@ public class ConsentServiceTest { @Test public void shouldCallSaveMethodInConsentRepository() { Consent consent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); consent.setEntityName("Teacher"); consent.setEntityId("123"); consent.setRequestorName("Institute"); @@ -36,7 +40,7 @@ public void shouldCallSaveMethodInConsentRepository() { map.put("name", 1); consent.setConsentFields(map); consent.setExpirationTime("1000"); - consent.setOsOwner("789"); + consent.setOsOwner(osOwners); consentService.saveConsent(consent); verify(consentRepository, times(1)).save(consent); } @@ -44,6 +48,8 @@ public void shouldCallSaveMethodInConsentRepository() { @Test public void shouldRetrieveConsentsBasedOnId() throws ConsentDefinitionNotFoundException { Consent expectedConsent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); expectedConsent.setEntityName("Teacher"); expectedConsent.setEntityId("123"); expectedConsent.setRequestorName("Institute"); @@ -52,7 +58,7 @@ public void shouldRetrieveConsentsBasedOnId() throws ConsentDefinitionNotFoundEx map.put("name", 1); expectedConsent.setConsentFields(map); expectedConsent.setExpirationTime("1000"); - expectedConsent.setOsOwner("789"); + expectedConsent.setOsOwner(osOwners); when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); Consent actualConsent = consentService.retrieveConsents("123"); verify(consentRepository, times(1)).findById("123"); @@ -68,6 +74,8 @@ public void shouldThrowExceptionIfConsentIsNotAvailableForId() { @Test public void shouldGrantConsent() throws Exception { Consent consent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); consent.setEntityName("Teacher"); consent.setEntityId("123"); consent.setRequestorName("Institute"); @@ -76,11 +84,33 @@ public void shouldGrantConsent() throws Exception { map.put("name", 1); consent.setConsentFields(map); consent.setExpirationTime("1000"); - consent.setOsOwner("789"); + consent.setOsOwner(osOwners); when(consentRepository.findById("123")).thenReturn(Optional.of(consent)); consentService.grantOrDenyConsent(GRANTED.name(), "123"); consent.setStatus(true); verify(consentRepository, times(1)).findById("123"); verify(consentRepository, times(1)).save(consent); } + + @Test + public void shouldRetrieveConsentByOwnerId() { + Consent consent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); + consent.setEntityName("Teacher"); + consent.setEntityId("123"); + consent.setRequestorName("Institute"); + consent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + consent.setConsentFields(map); + consent.setExpirationTime("1000"); + consent.setOsOwner(osOwners); + List consents = new ArrayList<>(); + consents.add(consent); + when(consentRepository.findByOsOwner("789")).thenReturn(consents); + consentService.retrieveConsentByOwnerId("789"); + verify(consentRepository, times(1)).findByOsOwner("789"); + + } } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java index 65dc04a5e..72544e159 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java @@ -135,6 +135,13 @@ public ResponseEntity getConsent(@PathVariable String consentId, HttpSer return new ResponseEntity<>(consentNotGranted, HttpStatus.OK); } + @GetMapping("/api/v1/consent") + public ResponseEntity getConsentByOwner(HttpServletRequest request) throws Exception { + String userId = registryHelper.getKeycloakUserId(request); + JsonNode node = consentRequestClient.getConsentByOwner(userId); + return new ResponseEntity<>(node, HttpStatus.OK); + } + @RequestMapping(value = "/partner/api/v1/{entityName}", method = RequestMethod.GET) public ResponseEntity getEntityWithConsent( @PathVariable String entityName, diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java index eb1828617..df643d47c 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java @@ -79,4 +79,8 @@ public ResponseEntity grantOrRejectClaim(String consentId, JsonNode json PluginRouter.route(pluginRequestMessage); return null; } + + public JsonNode getConsentByOwner(String ownerId) { + return restTemplate.getForObject(consentUrl + "/api/v1/consent/owner/" + ownerId, JsonNode.class); + } } diff --git a/java/registry/src/main/resources/application.yml b/java/registry/src/main/resources/application.yml index 5481c01c9..ff8939d2a 100644 --- a/java/registry/src/main/resources/application.yml +++ b/java/registry/src/main/resources/application.yml @@ -232,6 +232,8 @@ keycloak-user: email-actions: ${keycloack_user_email_actions:} claims: url: ${claims_url:http://localhost:8081} +consent: + url: ${consent_url:http://localhost:8083} authentication: enabled: ${authentication_enabled:true} publicKey: ${authentication_publickey:} From ad0379517ac3b2e2a17e2c517281dea05c93cb4b Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Thu, 8 Dec 2022 20:28:59 +0530 Subject: [PATCH 5/9] add health api --- docker-compose.yml | 5 +++++ .../dev/sunbirdrc/consent/controller/ConsentController.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index ee9a6ef19..2f04112b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -142,6 +142,11 @@ services: condition: service_started registry: condition: service_started + healthcheck: + test: [ "CMD-SHELL", "wget -nv -t1 --spider http://localhost:8083/health || exit 1" ] + interval: 30s + timeout: 10s + retries: 4 certificate-signer: image: dockerhub/sunbird-rc-certificate-signer:${RELEASE_VERSION} environment: diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java index 80b2940cc..c7016c583 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java @@ -48,4 +48,9 @@ public ResponseEntity> getConsentByOwnerId(@PathVariable String ow List consent = consentService.retrieveConsentByOwnerId(ownerId); return new ResponseEntity<>(consent, HttpStatus.OK); } + + @GetMapping(value = "/health") + public ResponseEntity health() { + return ResponseEntity.ok().build(); + } } From 00f0d37550d5b795c3ea636fec47c165815752fe Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Mon, 12 Dec 2022 16:09:22 +0530 Subject: [PATCH 6/9] add test cases and some code refactoring --- .../java/e2e/registry/ConsentRequest.json | 1 - .../consent/controller/ConsentController.java | 18 ++-- .../exceptions/ConsentForbiddenException.java | 7 ++ .../consent/service/ConsentService.java | 41 ++++++++- .../consent/service/ConsentServiceTest.java | 92 +++++++++++++++++-- .../controller/AbstractController.java | 2 +- .../controller/RegistryConsentController.java | 59 ++++-------- .../registry/util/ConsentRequestClient.java | 17 ++-- .../util/ConsentRequestClientTest.java | 65 +++++++++++++ .../sunbirdrc/actors/ConsentPluginActor.java | 6 +- 10 files changed, 236 insertions(+), 72 deletions(-) create mode 100644 java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentForbiddenException.java create mode 100644 java/registry/src/test/java/dev/sunbirdrc/registry/util/ConsentRequestClientTest.java diff --git a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json index 6e841df15..ef2cd6085 100644 --- a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json +++ b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json @@ -3,7 +3,6 @@ "entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d", "status": "false", "requestorName": "Random", - "requestorId": "1", "consentFieldsPath": { "name": "$.name", "country": "" diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java index c7016c583..7ffc3849d 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java @@ -2,6 +2,7 @@ import dev.sunbirdrc.consent.entity.Consent; import dev.sunbirdrc.consent.exceptions.ConsentDefinitionNotFoundException; +import dev.sunbirdrc.consent.exceptions.ConsentForbiddenException; import dev.sunbirdrc.consent.service.ConsentService; import dev.sunbirdrc.pojos.dto.ConsentDTO; import org.springframework.beans.factory.annotation.Autowired; @@ -27,16 +28,21 @@ public ResponseEntity createConsent(@RequestBody ConsentDTO consentDTO) return new ResponseEntity<>(savedConsent, HttpStatus.CREATED); } - @GetMapping(value = "/api/v1/consent/{id}") - public ResponseEntity getConsentById(@PathVariable String id) throws ConsentDefinitionNotFoundException { - Consent consent = consentService.retrieveConsents(id); + @GetMapping(value = "/api/v1/consent/{id}/{requestorId}") + public ResponseEntity getConsentById(@PathVariable String id, @PathVariable String requestorId) throws ConsentDefinitionNotFoundException, ConsentForbiddenException { + Consent consent = null; + try { + consent = consentService.retrieveConsents(id, requestorId); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } return new ResponseEntity<>(consent, HttpStatus.OK); } - @PutMapping(value = "/api/v1/consent/{id}") - public ResponseEntity grantOrDenyConsent(@PathVariable String id, @RequestBody Map statusMap) { + @PutMapping(value = "/api/v1/consent/{id}/{consenterId}") + public ResponseEntity grantOrDenyConsent(@PathVariable String id, @PathVariable String consenterId, @RequestBody Map statusMap) { try { - Consent consent = consentService.grantOrDenyConsent(statusMap.get(STATUS), id); + Consent consent = consentService.grantOrDenyConsent(statusMap.get(STATUS), id, consenterId); return new ResponseEntity<>(consent, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentForbiddenException.java b/java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentForbiddenException.java new file mode 100644 index 000000000..26bafd27f --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/exceptions/ConsentForbiddenException.java @@ -0,0 +1,7 @@ +package dev.sunbirdrc.consent.exceptions; + +public class ConsentForbiddenException extends Exception { + public ConsentForbiddenException(String consentTimeExpired) { + super(consentTimeExpired); + } +} diff --git a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java index 8bd6b6541..a4a3f207e 100644 --- a/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java @@ -2,10 +2,12 @@ import dev.sunbirdrc.consent.entity.Consent; import dev.sunbirdrc.consent.exceptions.ConsentDefinitionNotFoundException; +import dev.sunbirdrc.consent.exceptions.ConsentForbiddenException; import dev.sunbirdrc.consent.repository.ConsentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Date; import java.util.List; import java.util.Optional; @@ -20,18 +22,47 @@ public Consent saveConsent(Consent consent) { return consentRepository.save(consent); } - public Consent retrieveConsents(String id) throws ConsentDefinitionNotFoundException { - Consent consent = consentRepository.findById(id).orElseThrow(() -> new ConsentDefinitionNotFoundException("Invalid ID of consent")); + public Consent retrieveConsents(String id, String requestorId) throws ConsentDefinitionNotFoundException, ConsentForbiddenException { + Consent consent = consentRepository.findById(id) + .orElseThrow(() -> new ConsentDefinitionNotFoundException("Invalid ID of consent")); + boolean isGranted = consent.isStatus(); + boolean isOwner = requestorId.equals(consent.getRequestorId()); + if(!isOwner) { + final String forbidden = "You are not authorized to access this consent"; + throw new ConsentForbiddenException(forbidden); + } + if(!isGranted) { + throw new ConsentForbiddenException("Consent denied or not approved until now"); + } + if(isConsentTimeExpired(consent.getCreatedAt(), consent.getExpirationTime())) { + final String consentTimeExpired = "Consent Time Expired"; + throw new ConsentForbiddenException(consentTimeExpired); + } return consent; } - public Consent grantOrDenyConsent(String status, String id) throws Exception{ + private boolean isConsentTimeExpired(Date createdAt, String expirationTime) { + Date expirationAt = new Date(); + Date currentDate = new Date(); + expirationAt.setTime(createdAt.getTime() + Long.parseLong(expirationTime) * 1000); + return expirationAt.compareTo(currentDate) < 0; + } + + public Consent grantOrDenyConsent(String status, String id, String consenterId) throws Exception{ Optional optConsent = consentRepository.findById(id); Consent consent = optConsent.map(consent1 -> { - consent1.setStatus(status.equals(GRANTED.name())); - return consent1; + if(consent1.getOsOwner().contains(consenterId)) { + consent1.setStatus(status.equals(GRANTED.name())); + return consent1; + } + try { + throw new ConsentForbiddenException("You are not authorized to update this consent"); + } catch (ConsentForbiddenException e) { + throw new RuntimeException(e); + } }).orElse(null); if(consent == null) throw new ConsentDefinitionNotFoundException("Invalid ID of consent"); + return consentRepository.save(consent); } diff --git a/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java index 3830c3321..4e9278fc1 100644 --- a/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java +++ b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java @@ -2,6 +2,7 @@ import dev.sunbirdrc.consent.entity.Consent; import dev.sunbirdrc.consent.exceptions.ConsentDefinitionNotFoundException; +import dev.sunbirdrc.consent.exceptions.ConsentForbiddenException; import dev.sunbirdrc.consent.repository.ConsentRepository; import org.junit.Test; import org.junit.runner.RunWith; @@ -9,10 +10,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; +import java.util.*; import static dev.sunbirdrc.consent.constants.ConstentStatus.GRANTED; import static org.junit.Assert.assertEquals; @@ -46,12 +44,14 @@ public void shouldCallSaveMethodInConsentRepository() { } @Test - public void shouldRetrieveConsentsBasedOnId() throws ConsentDefinitionNotFoundException { + public void shouldRetrieveConsentsBasedOnId() throws ConsentDefinitionNotFoundException, ConsentForbiddenException { Consent expectedConsent = new Consent(); List osOwners = new ArrayList<>(); osOwners.add("789"); expectedConsent.setEntityName("Teacher"); expectedConsent.setEntityId("123"); + expectedConsent.setStatus(true); + expectedConsent.setCreatedAt(new Date()); expectedConsent.setRequestorName("Institute"); expectedConsent.setRequestorId("456"); HashMap map = new HashMap(); @@ -60,15 +60,91 @@ public void shouldRetrieveConsentsBasedOnId() throws ConsentDefinitionNotFoundEx expectedConsent.setExpirationTime("1000"); expectedConsent.setOsOwner(osOwners); when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); - Consent actualConsent = consentService.retrieveConsents("123"); + Consent actualConsent = consentService.retrieveConsents("123", "456"); verify(consentRepository, times(1)).findById("123"); assertEquals(expectedConsent, actualConsent); } + @Test + public void shouldThrowExceptionWhenConsentNotGranted() throws ConsentDefinitionNotFoundException, ConsentForbiddenException { + Consent expectedConsent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); + expectedConsent.setEntityName("Teacher"); + expectedConsent.setEntityId("123"); + expectedConsent.setStatus(false); + expectedConsent.setCreatedAt(new Date()); + expectedConsent.setRequestorName("Institute"); + expectedConsent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + expectedConsent.setConsentFields(map); + expectedConsent.setExpirationTime("1000"); + expectedConsent.setOsOwner(osOwners); + when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); + String expectedMessage = "Consent denied or not approved until now"; + String message = assertThrows(ConsentForbiddenException.class, () -> { + consentService.retrieveConsents("123", "456"); + }).getMessage(); + assertEquals(expectedMessage, message); + } + + @Test + public void shouldThrowExceptionWhenConsentTimeExpired() throws ConsentDefinitionNotFoundException, ConsentForbiddenException { + Consent expectedConsent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); + expectedConsent.setEntityName("Teacher"); + expectedConsent.setEntityId("123"); + expectedConsent.setStatus(true); + Date d = new Date(); + d.setTime(d.getTime() - 1001 * 1000); + expectedConsent.setCreatedAt(d); + expectedConsent.setRequestorName("Institute"); + expectedConsent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + expectedConsent.setConsentFields(map); + expectedConsent.setExpirationTime("1000"); + expectedConsent.setOsOwner(osOwners); + when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); + String message = assertThrows(ConsentForbiddenException.class, () -> { + consentService.retrieveConsents("123", "456"); + }).getMessage(); + String expectedMessage = "Consent Time Expired"; + assertEquals(expectedMessage, message); + } + + @Test + public void shouldThrowExceptionWhenConsentOwnerIsIncorrect() throws ConsentDefinitionNotFoundException, ConsentForbiddenException { + Consent expectedConsent = new Consent(); + List osOwners = new ArrayList<>(); + osOwners.add("789"); + expectedConsent.setEntityName("Teacher"); + expectedConsent.setEntityId("123"); + expectedConsent.setStatus(true); + Date d = new Date(); + d.setTime(d.getTime() - 1001 * 1000); + expectedConsent.setCreatedAt(d); + expectedConsent.setRequestorName("Institute"); + expectedConsent.setRequestorId("457"); + HashMap map = new HashMap(); + map.put("name", 1); + expectedConsent.setConsentFields(map); + expectedConsent.setExpirationTime("1000"); + expectedConsent.setOsOwner(osOwners); + when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); + String message = assertThrows(ConsentForbiddenException.class, () -> { + consentService.retrieveConsents("123", "456"); + }).getMessage(); + String expectedMessage = "You are not authorized to access this consent"; + assertEquals(expectedMessage, message); + } + @Test public void shouldThrowExceptionIfConsentIsNotAvailableForId() { when(consentRepository.findById("123")).thenReturn(Optional.ofNullable(null)); - assertThrows(ConsentDefinitionNotFoundException.class, () -> consentService.retrieveConsents("123")); + assertThrows(ConsentDefinitionNotFoundException.class, () -> consentService.retrieveConsents("123", "456")); } @Test @@ -86,7 +162,7 @@ public void shouldGrantConsent() throws Exception { consent.setExpirationTime("1000"); consent.setOsOwner(osOwners); when(consentRepository.findById("123")).thenReturn(Optional.of(consent)); - consentService.grantOrDenyConsent(GRANTED.name(), "123"); + consentService.grantOrDenyConsent(GRANTED.name(), "123", "789"); consent.setStatus(true); verify(consentRepository, times(1)).findById("123"); verify(consentRepository, times(1)).save(consent); diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java index ec4f405a8..08a69a223 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/AbstractController.java @@ -65,7 +65,7 @@ ResponseEntity internalErrorResponse(ResponseParams responseParams, Resp ResponseEntity forbiddenExceptionResponse(Exception e) { ResponseParams responseParams = new ResponseParams(); - Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); + Response response = new Response(Response.API_ID.READ, "OK", responseParams); responseParams.setErrmsg(e.getMessage()); responseParams.setStatus(Response.Status.UNSUCCESSFUL); return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java index 72544e159..e468c5815 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java @@ -98,41 +98,24 @@ public ResponseEntity getConsent(@PathVariable String consentId, HttpSer JsonNode consent = null; ResponseParams responseParams = new ResponseParams(); Response response = new Response(Response.API_ID.READ, "OK", responseParams); - try { - consent = consentRequestClient.getConsent(consentId); - } catch (Exception exception) { - return internalErrorResponse(responseParams, response, exception); - } - boolean status = consent.get("status").asText().equals("true"); String keycloakUserId = registryHelper.getKeycloakUserId(request); - boolean isOwner = keycloakUserId.equals(consent.get("requestorId").asText()); try { - if(isConsentTimeExpired(consent.get("createdAt").asText(), consent.get("expirationTime").asText())) { - final String consentTimeExpired = "Consent Time Expired"; - throw new ConsentForbiddenException(consentTimeExpired); - } - if(!isOwner) { - final String forbidden = "You are not authorized to access this consent"; - throw new ConsentForbiddenException(forbidden); - } - } catch (ConsentForbiddenException consentForbiddenException) { - return forbiddenExceptionResponse(consentForbiddenException); + consent = consentRequestClient.getConsentByConsentIdAndCreator(consentId, keycloakUserId); + } catch (Exception e) { + return forbiddenExceptionResponse(e); } - if(status) { - JsonNode userInfoFromRegistry = null; - String[] osOwners = consent.get("osOwner").asText().split(","); - String entityName = consent.get("entityName").asText(); - for(String owner : osOwners) { - userInfoFromRegistry = consentRequestClient.searchUser(entityName, owner); - if (userInfoFromRegistry != null) - break; - } - ArrayList consentFields = new ArrayList<>(objectMapper.convertValue(consent.get("consentFields"), Map.class).keySet()); - ResponseEntity entityNode = getJsonNodeResponseEntity(userInfoFromRegistry, entityName, consentFields); - if (entityNode != null) return entityNode; + JsonNode userInfoFromRegistry = null; + String[] osOwners = consent.get("osOwner").asText().split(","); + String entityName = consent.get("entityName").asText(); + for(String owner : osOwners) { + userInfoFromRegistry = consentRequestClient.searchUser(entityName, owner); + if (userInfoFromRegistry != null) + break; } - final String consentNotGranted = "Consent is rejected or not granted until now"; - return new ResponseEntity<>(consentNotGranted, HttpStatus.OK); + ArrayList consentFields = new ArrayList<>(objectMapper.convertValue(consent.get("consentFields"), Map.class).keySet()); + ResponseEntity entityNode = getJsonNodeResponseEntity(userInfoFromRegistry, entityName, consentFields); + response.setResult(entityNode.getBody()); + return new ResponseEntity<>(response, HttpStatus.OK); } @GetMapping("/api/v1/consent") @@ -166,18 +149,12 @@ public ResponseEntity getEntityWithConsent( public ResponseEntity grantOrRejectClaim(@PathVariable String consentId, @RequestBody JsonNode jsonNode, HttpServletRequest request) throws Exception { ResponseParams responseParams = new ResponseParams(); Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); - JsonNode consent; + String userId = registryHelper.getKeycloakUserId(request); try { - consent = consentRequestClient.getConsent(consentId); + response.setResult(consentRequestClient.grantOrRejectClaim(consentId, userId, jsonNode)); } catch (Exception e) { - return internalErrorResponse(responseParams, response, e); - } - String userId = registryHelper.getUserId(request, consent.get("entityName").asText()); - String[] osOwners = consent.get("osOwner").asText().split(","); - boolean isOwner = Arrays.stream(osOwners).filter(owner -> owner.equals(userId)) != null; - if(isOwner) { - return consentRequestClient.grantOrRejectClaim(consentId, jsonNode); + return forbiddenExceptionResponse(e); } - return forbiddenExceptionResponse(new ConsentForbiddenException("You are not authorized to update this consent")); + return new ResponseEntity<>(response, HttpStatus.OK); } } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java index df643d47c..c3208eeec 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java @@ -37,13 +37,6 @@ public ConsentRequestClient(@Value("${consent.url}") String consentUrl, RestTemp this.restTemplate = restTemplate; } - public JsonNode getConsent(String consentId) throws Exception{ - return restTemplate.getForObject( - consentUrl + "/api/v1/consent/" + consentId, - JsonNode.class - ); - } - public JsonNode searchUser(String entityName, String userId) throws Exception { ObjectNode payload = JsonNodeFactory.instance.objectNode(); payload.set(ENTITY_TYPE, JsonNodeFactory.instance.arrayNode().add(entityName)); @@ -68,10 +61,11 @@ public void addConsent(ObjectNode objectNode, HttpServletRequest request) throws PluginRouter.route(pluginRequestMessage); } - public ResponseEntity grantOrRejectClaim(String consentId, JsonNode jsonNode) throws Exception { + public ResponseEntity grantOrRejectClaim(String consentId, String userId, JsonNode jsonNode) throws Exception { final String attestorPlugin = "did:internal:ConsentPluginActor"; PluginRequestMessage pluginRequestMessage = PluginRequestMessage.builder().build(); pluginRequestMessage.setAttestorPlugin(attestorPlugin); + pluginRequestMessage.setUserId(userId); pluginRequestMessage.setStatus(jsonNode.get("status").asText()); String consent = "{\"consentId\" : " + "\"" + consentId + "\"" + "}"; JsonNode additionalInput = objectMapper.readValue(consent, JsonNode.class); @@ -83,4 +77,11 @@ public ResponseEntity grantOrRejectClaim(String consentId, JsonNode json public JsonNode getConsentByOwner(String ownerId) { return restTemplate.getForObject(consentUrl + "/api/v1/consent/owner/" + ownerId, JsonNode.class); } + + public JsonNode getConsentByConsentIdAndCreator(String consentId, String keycloakUserId) { + return restTemplate.getForObject( + consentUrl + "/api/v1/consent/" + consentId + "/" + keycloakUserId, + JsonNode.class + ); + } } diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/util/ConsentRequestClientTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/util/ConsentRequestClientTest.java new file mode 100644 index 000000000..145d36c19 --- /dev/null +++ b/java/registry/src/test/java/dev/sunbirdrc/registry/util/ConsentRequestClientTest.java @@ -0,0 +1,65 @@ +package dev.sunbirdrc.registry.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.sunbirdrc.registry.helper.RegistryHelper; +import dev.sunbirdrc.registry.middleware.util.OSSystemFields; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import static dev.sunbirdrc.registry.middleware.util.Constants.ENTITY_TYPE; +import static dev.sunbirdrc.registry.middleware.util.Constants.FILTERS; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(MockitoJUnitRunner.class) +public class ConsentRequestClientTest { + + @Mock + private RegistryHelper registryHelper; + @Mock + private ObjectMapper objectMapper; + @Mock + RestTemplate restTemplate; + @InjectMocks + private ConsentRequestClient consentRequestClient; + @Before + public void setUp() throws Exception { + ReflectionTestUtils.setField(consentRequestClient, "consentUrl", "localhost:8083"); + ReflectionTestUtils.setField(consentRequestClient, "registryHelper", registryHelper); + ReflectionTestUtils.setField(consentRequestClient, "objectMapper", objectMapper); + + } + + @Test + public void shouldCallGetConsentByIdAndCreator() { + consentRequestClient.getConsentByConsentIdAndCreator("123", "456"); + verify(restTemplate, times(1)).getForObject( + "localhost:8083/api/v1/consent/123/456", JsonNode.class); + } + + @Test + public void shouldSearchUser() throws Exception { + ObjectNode expectedPayload = JsonNodeFactory.instance.objectNode(); + expectedPayload.set(ENTITY_TYPE, JsonNodeFactory.instance.arrayNode().add("temp")); + ObjectNode filters = JsonNodeFactory.instance.objectNode(); + filters.set(OSSystemFields.osOwner.toString(), JsonNodeFactory.instance.objectNode().put("contains", "123")); + expectedPayload.set(FILTERS, filters); + consentRequestClient.searchUser("temp", "123"); + verify(registryHelper, times(1)).searchEntity(expectedPayload); + } + + @Test + public void shouldGetConsentByOwner() { + consentRequestClient.getConsentByOwner("123"); + verify(restTemplate, times(1)).getForObject("localhost:8083/api/v1/consent/owner/123", JsonNode.class); + } +} diff --git a/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java b/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java index fc95e25bb..902f50858 100644 --- a/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java +++ b/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.NullNode; import dev.sunbirdrc.pojos.PluginRequestMessage; import dev.sunbirdrc.pojos.dto.ConsentDTO; import org.springframework.beans.factory.annotation.Autowired; @@ -24,7 +25,7 @@ public class ConsentPluginActor extends BaseActor { @Override protected void onReceive(MessageProtos.Message request) throws Throwable { PluginRequestMessage pluginRequestMessage = new ObjectMapper().readValue(request.getPayload().getStringValue(), PluginRequestMessage.class); - if(pluginRequestMessage.getUserId() == null) { + if(!(pluginRequestMessage.getAdditionalInputs() instanceof NullNode)) { grantOrRejectConsent(pluginRequestMessage); return; } @@ -34,8 +35,9 @@ protected void onReceive(MessageProtos.Message request) throws Throwable { private void grantOrRejectConsent(PluginRequestMessage pluginRequestMessage) throws IOException { String requestBody = "{\"status\": " + "\"" + pluginRequestMessage.getStatus() + "\"" + "}"; String consentId = pluginRequestMessage.getAdditionalInputs().get("consentId").asText(); + String consenterId = pluginRequestMessage.getUserId(); JsonNode jsonNode = new ObjectMapper().readValue(requestBody, JsonNode.class); - restTemplate.exchange(consentUrl + "/api/v1/consent/" + consentId, HttpMethod.PUT,new HttpEntity<>(jsonNode), Object.class); + restTemplate.exchange(consentUrl + "/api/v1/consent/" + consentId + "/" + consenterId, HttpMethod.PUT,new HttpEntity<>(jsonNode), Object.class); } private void createConsent(PluginRequestMessage pluginRequestMessage) { From d4f471d11f93087a5034feeefa25a518e59fba6c Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Mon, 12 Dec 2022 16:13:37 +0530 Subject: [PATCH 7/9] add make script for consent service --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 067b754ac..0a315eeea 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ build: java/registry/target/registry.jar rm -rf java/claim/target/*.jar cd target && rm -rf * && jar xvf ../java/registry/target/registry.jar && cp ../java/Dockerfile ./ && docker build -t dockerhub/sunbird-rc-core . make -C java/claim + make -C java/consent make -C services/certificate-api docker make -C services/certificate-signer docker make -C services/notification-service docker From 2604e2d0e091405c10587a5e1c602f9c6400bec0 Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Sat, 17 Dec 2022 22:39:24 +0530 Subject: [PATCH 8/9] remove objectnode as a parameter for consent api and replace with pojo --- .../test/java/e2e/registry/ConsentRequest.json | 1 - .../controller/RegistryConsentController.java | 5 +++-- .../registry/util/ConsentRequestClient.java | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json index ef2cd6085..325a4de19 100644 --- a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json +++ b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json @@ -1,7 +1,6 @@ { "entityName": "Place", "entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d", - "status": "false", "requestorName": "Random", "consentFieldsPath": { "name": "$.name", diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java index e468c5815..e56a5aa7f 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import dev.sunbirdrc.pojos.Response; import dev.sunbirdrc.pojos.ResponseParams; +import dev.sunbirdrc.pojos.dto.ConsentDTO; import dev.sunbirdrc.registry.exception.ConsentForbiddenException; import dev.sunbirdrc.registry.exception.RecordNotFoundException; import dev.sunbirdrc.registry.helper.RegistryHelper; @@ -86,9 +87,9 @@ private ObjectNode copyWhiteListedFields(ArrayList fields, JsonNode data } @PostMapping(value = "/api/v1/consent") - public ResponseEntity createConsent(@RequestBody ObjectNode objectNode, HttpServletRequest request) throws Exception { + public ResponseEntity createConsent(@RequestBody ConsentDTO consentDTO, HttpServletRequest request) throws Exception { ResponseParams responseParams = new ResponseParams(); - consentRequestClient.addConsent(objectNode, request); + consentRequestClient.addConsent(consentDTO, request); responseParams.setStatus(Response.Status.SUCCESSFUL); return new ResponseEntity<>(responseParams, HttpStatus.OK); } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java index c3208eeec..af27c2bdf 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import dev.sunbirdrc.actors.factory.PluginRouter; import dev.sunbirdrc.pojos.PluginRequestMessage; +import dev.sunbirdrc.pojos.dto.ConsentDTO; import dev.sunbirdrc.registry.helper.RegistryHelper; import dev.sunbirdrc.registry.middleware.util.OSSystemFields; import org.springframework.beans.factory.annotation.Autowired; @@ -46,18 +47,17 @@ public JsonNode searchUser(String entityName, String userId) throws Exception { return registryHelper.searchEntity(payload); } - public void addConsent(ObjectNode objectNode, HttpServletRequest request) throws Exception { + public void addConsent(ConsentDTO consentDTO, HttpServletRequest request) throws Exception { final String attestorPlugin = "did:internal:ConsentPluginActor"; PluginRequestMessage pluginRequestMessage = PluginRequestMessage.builder().build(); pluginRequestMessage.setAttestorPlugin(attestorPlugin); - pluginRequestMessage.setConsentEntityName(objectNode.get("entityName").asText()); - pluginRequestMessage.setConsentEntityId(objectNode.get("entityId").asText()); - pluginRequestMessage.setExpirationTime(objectNode.get("consentExpiryTime").asText()); - pluginRequestMessage.setSourceEntity(objectNode.get("requestorName").asText()); + pluginRequestMessage.setConsentEntityName(consentDTO.getEntityName()); + pluginRequestMessage.setConsentEntityId(consentDTO.getEntityId()); + pluginRequestMessage.setExpirationTime(consentDTO.getConsentExpiryTime()); + pluginRequestMessage.setSourceEntity(consentDTO.getRequestorName()); pluginRequestMessage.setUserId(registryHelper.getKeycloakUserId(request)); - pluginRequestMessage.setStatus(objectNode.get("status").asText()); - pluginRequestMessage.setConsentFieldPath(objectMapper.convertValue(objectNode.get("consentFieldsPath"), Map.class)); - pluginRequestMessage.setConsentEntityOsOwner(objectMapper.convertValue(objectNode.get("osOwner"), List.class)); + pluginRequestMessage.setConsentFieldPath(consentDTO.getConsentFieldsPath()); + pluginRequestMessage.setConsentEntityOsOwner(consentDTO.getOsOwner()); PluginRouter.route(pluginRequestMessage); } From 02b60af9a291fd7a0512c248017cbf74a00ebd8f Mon Sep 17 00:00:00 2001 From: Tejas Varade Date: Tue, 20 Dec 2022 11:49:08 +0530 Subject: [PATCH 9/9] include only private fields for consent access --- .../src/test/java/e2e/registry/ConsentRequest.json | 3 +-- .../java/e2e/registry/FailingConsentRequest.json | 11 +++++++++++ .../src/test/java/e2e/registry/registry.feature | 12 ++++++++++++ .../controller/RegistryConsentController.java | 8 +++++++- .../registry/util/ConsentRequestClient.java | 14 +++++++++++++- 5 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 java/apitest/src/test/java/e2e/registry/FailingConsentRequest.json diff --git a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json index 325a4de19..8b93a431d 100644 --- a/java/apitest/src/test/java/e2e/registry/ConsentRequest.json +++ b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json @@ -3,8 +3,7 @@ "entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d", "requestorName": "Random", "consentFieldsPath": { - "name": "$.name", - "country": "" + "name": "$.name" }, "osOwner": ["c8211527-9d2f-4e08-8d8a-b57ef8be6eba","anonymous"], "consentExpiryTime": "1000" diff --git a/java/apitest/src/test/java/e2e/registry/FailingConsentRequest.json b/java/apitest/src/test/java/e2e/registry/FailingConsentRequest.json new file mode 100644 index 000000000..325a4de19 --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/FailingConsentRequest.json @@ -0,0 +1,11 @@ +{ + "entityName": "Place", + "entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d", + "requestorName": "Random", + "consentFieldsPath": { + "name": "$.name", + "country": "" + }, + "osOwner": ["c8211527-9d2f-4e08-8d8a-b57ef8be6eba","anonymous"], + "consentExpiryTime": "1000" +} \ No newline at end of file diff --git a/java/apitest/src/test/java/e2e/registry/registry.feature b/java/apitest/src/test/java/e2e/registry/registry.feature index 846ec0a0b..83e1a8d3d 100644 --- a/java/apitest/src/test/java/e2e/registry/registry.feature +++ b/java/apitest/src/test/java/e2e/registry/registry.feature @@ -251,6 +251,18 @@ Feature: Registry api tests Then status 200 * sleep(3000) +# create consent for entity Place but without private fields + Given url registryUrl + And path 'api/v1/consent' + And header Authorization = admin_token + * def consentRequest = read('FailingConsentRequest.json') + * consentRequest.entityId = placeOsid + * consentRequest.osOwner = placeOwner + And request consentRequest + When method post + Then status 500 + * sleep(3000) + # fetch consent by owner Given url registryUrl And path 'api/v1/consent/' diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java index e56a5aa7f..fd0763051 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java @@ -89,7 +89,13 @@ private ObjectNode copyWhiteListedFields(ArrayList fields, JsonNode data @PostMapping(value = "/api/v1/consent") public ResponseEntity createConsent(@RequestBody ConsentDTO consentDTO, HttpServletRequest request) throws Exception { ResponseParams responseParams = new ResponseParams(); - consentRequestClient.addConsent(consentDTO, request); + try { + consentRequestClient.addConsent(consentDTO, request); + } catch (Exception e) { + responseParams.setStatus(Response.Status.UNSUCCESSFUL); + responseParams.setErrmsg(e.getMessage()); + return new ResponseEntity<>(responseParams, HttpStatus.INTERNAL_SERVER_ERROR); + } responseParams.setStatus(Response.Status.SUCCESSFUL); return new ResponseEntity<>(responseParams, HttpStatus.OK); } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java index af27c2bdf..a576ce626 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java @@ -7,6 +7,7 @@ import dev.sunbirdrc.actors.factory.PluginRouter; import dev.sunbirdrc.pojos.PluginRequestMessage; import dev.sunbirdrc.pojos.dto.ConsentDTO; +import dev.sunbirdrc.registry.exception.ConsentForbiddenException; import dev.sunbirdrc.registry.helper.RegistryHelper; import dev.sunbirdrc.registry.middleware.util.OSSystemFields; import org.springframework.beans.factory.annotation.Autowired; @@ -16,7 +17,6 @@ import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; -import java.util.List; import java.util.Map; import static dev.sunbirdrc.registry.middleware.util.Constants.ENTITY_TYPE; @@ -28,6 +28,8 @@ public class ConsentRequestClient { private final String consentUrl; private final RestTemplate restTemplate; + @Autowired + IDefinitionsManager definitionsManager; @Autowired private ObjectMapper objectMapper; @Autowired @@ -48,6 +50,10 @@ public JsonNode searchUser(String entityName, String userId) throws Exception { } public void addConsent(ConsentDTO consentDTO, HttpServletRequest request) throws Exception { + if(!(definitionsManager.getDefinition(consentDTO.getEntityName()) != null && + isAllConsentFieldsInPrivate(consentDTO.getConsentFieldsPath(), consentDTO.getEntityName()))) { + throw new ConsentForbiddenException("Consent cannot be requested on these fields"); + } final String attestorPlugin = "did:internal:ConsentPluginActor"; PluginRequestMessage pluginRequestMessage = PluginRequestMessage.builder().build(); pluginRequestMessage.setAttestorPlugin(attestorPlugin); @@ -61,6 +67,12 @@ public void addConsent(ConsentDTO consentDTO, HttpServletRequest request) throws PluginRouter.route(pluginRequestMessage); } + private boolean isAllConsentFieldsInPrivate(Map consentFieldsPath, String entityName) { + return consentFieldsPath.keySet().stream().allMatch(field -> + definitionsManager.getDefinition(entityName).getOsSchemaConfiguration().getPrivateFields().contains(field) + ); + } + public ResponseEntity grantOrRejectClaim(String consentId, String userId, JsonNode jsonNode) throws Exception { final String attestorPlugin = "did:internal:ConsentPluginActor"; PluginRequestMessage pluginRequestMessage = PluginRequestMessage.builder().build();