diff --git a/Makefile b/Makefile index 3cb23c34d..2ec5b92f0 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 diff --git a/docker-compose.yml b/docker-compose.yml index 096b993f4..86148252b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,6 +50,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 @@ -132,6 +133,25 @@ 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 + 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/apitest/src/test/java/e2e/registry/ConsentRequest.json b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json new file mode 100644 index 000000000..8b93a431d --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/ConsentRequest.json @@ -0,0 +1,10 @@ +{ + "entityName": "Place", + "entityId": "1-eab84a8b-72f2-448e-af65-d2082bee589d", + "requestorName": "Random", + "consentFieldsPath": { + "name": "$.name" + }, + "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/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/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/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..59ed95504 --- /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\": [{\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 72fcda46c..363a44b83 100644 --- a/java/apitest/src/test/java/e2e/registry/registry.feature +++ b/java/apitest/src/test/java/e2e/registry/registry.feature @@ -7,6 +7,8 @@ Feature: Registry api tests * def admin_token = "" * def client_secret = 'a52c5f4a-89fd-40b9-aea2-3f711f14c889' * def sleep = function(millis){ java.lang.Thread.sleep(millis) } + * def placeOsid = "" + * def placeOwner = "" Scenario: health check Given path 'health' @@ -191,6 +193,112 @@ Feature: Registry api tests 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) + +# 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/' + 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 @env=async Scenario: Create a teacher schema and create teacher entity asynchronously # get admin token @@ -323,5 +431,4 @@ Feature: Registry api tests And header Authorization = institute_token When method get Then status 200 - And response[0].osid.length > 0 - + And response[0].osid.length > 0 \ No newline at end of file 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/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/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 diff --git a/java/consent/pom.xml b/java/consent/pom.xml new file mode 100644 index 000000000..9abee7c09 --- /dev/null +++ b/java/consent/pom.xml @@ -0,0 +1,95 @@ + + + 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 + + + dev.sunbirdrc + pojos + 2.0.3 + compile + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.12.0 + test + + + + + + + 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..7ffc3849d --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/controller/ConsentController.java @@ -0,0 +1,62 @@ +package dev.sunbirdrc.consent.controller; + +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; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +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}/{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}/{consenterId}") + public ResponseEntity grantOrDenyConsent(@PathVariable String id, @PathVariable String consenterId, @RequestBody Map statusMap) { + try { + Consent consent = consentService.grantOrDenyConsent(statusMap.get(STATUS), id, consenterId); + return new ResponseEntity<>(consent, HttpStatus.OK); + } catch (Exception e) { + 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); + } + + @GetMapping(value = "/health") + public ResponseEntity health() { + return ResponseEntity.ok().build(); + } +} 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..c08d6422b --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/entity/Consent.java @@ -0,0 +1,77 @@ +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.List; +import java.util.Map; + +@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; + + @ElementCollection + @CollectionTable(name = "consent_owner_list", joinColumns = @JoinColumn(name = "id")) + @Column + private List 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(); + 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/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/repository/ConsentRepository.java b/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java new file mode 100644 index 000000000..d150fe389 --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/repository/ConsentRepository.java @@ -0,0 +1,12 @@ +package dev.sunbirdrc.consent.repository; + +import dev.sunbirdrc.consent.entity.Consent; +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 new file mode 100644 index 000000000..a4a3f207e --- /dev/null +++ b/java/consent/src/main/java/dev/sunbirdrc/consent/service/ConsentService.java @@ -0,0 +1,72 @@ +package dev.sunbirdrc.consent.service; + +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; + +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, 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; + } + + 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 -> { + 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); + } + + public List retrieveConsentByOwnerId(String ownerId) { + return consentRepository.findByOsOwner(ownerId); + } +} 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..93a47a542 --- /dev/null +++ b/java/consent/src/test/java/dev/sunbirdrc/consent/ConsentApplicationTests.java @@ -0,0 +1,13 @@ +package dev.sunbirdrc.consent; + +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ConsentApplicationTests { + + @Test + void contextLoads() { + } + +} 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..4e9278fc1 --- /dev/null +++ b/java/consent/src/test/java/dev/sunbirdrc/consent/service/ConsentServiceTest.java @@ -0,0 +1,192 @@ +package dev.sunbirdrc.consent.service; + +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.*; + +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(); + 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); + consentService.saveConsent(consent); + verify(consentRepository, times(1)).save(consent); + } + + @Test + 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(); + map.put("name", 1); + expectedConsent.setConsentFields(map); + expectedConsent.setExpirationTime("1000"); + expectedConsent.setOsOwner(osOwners); + when(consentRepository.findById("123")).thenReturn(Optional.of(expectedConsent)); + 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", "456")); + } + + @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"); + consent.setRequestorId("456"); + HashMap map = new HashMap(); + map.put("name", 1); + consent.setConsentFields(map); + consent.setExpirationTime("1000"); + consent.setOsOwner(osOwners); + when(consentRepository.findById("123")).thenReturn(Optional.of(consent)); + consentService.grantOrDenyConsent(GRANTED.name(), "123", "789"); + 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/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/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 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..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 @@ -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.READ, "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..fd0763051 --- /dev/null +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryConsentController.java @@ -0,0 +1,167 @@ +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.pojos.dto.ConsentDTO; +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 ConsentDTO consentDTO, HttpServletRequest request) throws Exception { + ResponseParams responseParams = new ResponseParams(); + 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); + } + + @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); + String keycloakUserId = registryHelper.getKeycloakUserId(request); + try { + consent = consentRequestClient.getConsentByConsentIdAndCreator(consentId, keycloakUserId); + } catch (Exception e) { + return forbiddenExceptionResponse(e); + } + 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); + response.setResult(entityNode.getBody()); + return new ResponseEntity<>(response, 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, + 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); + String userId = registryHelper.getKeycloakUserId(request); + try { + response.setResult(consentRequestClient.grantOrRejectClaim(consentId, userId, jsonNode)); + } catch (Exception e) { + return forbiddenExceptionResponse(e); + } + return new ResponseEntity<>(response, HttpStatus.OK); + } +} 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..a576ce626 --- /dev/null +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/ConsentRequestClient.java @@ -0,0 +1,99 @@ +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.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; +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.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 + IDefinitionsManager definitionsManager; + @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 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(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); + pluginRequestMessage.setConsentEntityName(consentDTO.getEntityName()); + pluginRequestMessage.setConsentEntityId(consentDTO.getEntityId()); + pluginRequestMessage.setExpirationTime(consentDTO.getConsentExpiryTime()); + pluginRequestMessage.setSourceEntity(consentDTO.getRequestorName()); + pluginRequestMessage.setUserId(registryHelper.getKeycloakUserId(request)); + pluginRequestMessage.setConsentFieldPath(consentDTO.getConsentFieldsPath()); + pluginRequestMessage.setConsentEntityOsOwner(consentDTO.getOsOwner()); + 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(); + pluginRequestMessage.setAttestorPlugin(attestorPlugin); + pluginRequestMessage.setUserId(userId); + 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; + } + + 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/main/resources/application.yml b/java/registry/src/main/resources/application.yml index 00a30a699..5f0811d0a 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:} 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 new file mode 100644 index 000000000..902f50858 --- /dev/null +++ b/java/sunbirdrc-actors/src/main/java/dev/sunbirdrc/actors/ConsentPluginActor.java @@ -0,0 +1,54 @@ +package dev.sunbirdrc.actors; + +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; +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.getAdditionalInputs() instanceof NullNode)) { + grantOrRejectConsent(pluginRequestMessage); + return; + } + createConsent(pluginRequestMessage); + } + + 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 + "/" + consenterId, 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