Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: Consent Module where a entity can ask for consent to access some fields of another entity #194

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Prev Previous commit
Next Next commit
add test cases and some code refactoring
varadeth committed Dec 12, 2022
commit 00f0d37550d5b795c3ea636fec47c165815752fe
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
"entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d",
"status": "false",
"requestorName": "Random",
"requestorId": "1",
"consentFieldsPath": {
"name": "$.name",
"country": ""
Original file line number Diff line number Diff line change
@@ -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<Consent> createConsent(@RequestBody ConsentDTO consentDTO)
return new ResponseEntity<>(savedConsent, HttpStatus.CREATED);
}

@GetMapping(value = "/api/v1/consent/{id}")
public ResponseEntity<Consent> getConsentById(@PathVariable String id) throws ConsentDefinitionNotFoundException {
Consent consent = consentService.retrieveConsents(id);
@GetMapping(value = "/api/v1/consent/{id}/{requestorId}")
public ResponseEntity<Object> 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<Consent> grantOrDenyConsent(@PathVariable String id, @RequestBody Map<String, String> statusMap) {
@PutMapping(value = "/api/v1/consent/{id}/{consenterId}")
public ResponseEntity<Consent> grantOrDenyConsent(@PathVariable String id, @PathVariable String consenterId, @RequestBody Map<String, String> 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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.sunbirdrc.consent.exceptions;

public class ConsentForbiddenException extends Exception {
public ConsentForbiddenException(String consentTimeExpired) {
super(consentTimeExpired);
}
}
Original file line number Diff line number Diff line change
@@ -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<Consent> 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);
}

Original file line number Diff line number Diff line change
@@ -2,17 +2,15 @@

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;
import org.mockito.InjectMocks;
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<String> 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<String> 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<String> 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<String> 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);
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@ ResponseEntity<Object> internalErrorResponse(ResponseParams responseParams, Resp

ResponseEntity<Object> 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);
Original file line number Diff line number Diff line change
@@ -98,41 +98,24 @@ public ResponseEntity<Object> 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<String> consentFields = new ArrayList<>(objectMapper.convertValue(consent.get("consentFields"), Map.class).keySet());
ResponseEntity<Object> 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<String> consentFields = new ArrayList<>(objectMapper.convertValue(consent.get("consentFields"), Map.class).keySet());
ResponseEntity<Object> 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<Object> getEntityWithConsent(
public ResponseEntity<Object> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> grantOrRejectClaim(String consentId, JsonNode jsonNode) throws Exception {
public ResponseEntity<Object> 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<Object> 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
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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) {