Overview
This document highlights the changes that need to be done in a Property module to support the privacy feature.
Pre-requisites
Prior Knowledge of Java/J2EE.
Prior Knowledge of Spring Boot.
MDMS Changes
Update the SecurityPolicy.json file.
The Security Policy MDMS file must have the model configuration for fields to be encrypted/decrypted.
The following models have been used for W&S in SecurityPolicy.json file:
Copy {
"model": "Property",
"uniqueIdentifier": {
"name": "uuid",
"jsonPath": "/owners/0/uuid"
},
"attributes": [
{
"name": "street",
"jsonPath": "address/street",
"patternId": "005",
"defaultVisibility": "PLAIN"
},
{
"name": "doorNo",
"jsonPath": "address/doorNo",
"patternId": "005",
"defaultVisibility": "PLAIN"
},
{
"name": "landmark",
"jsonPath": "address/landmark",
"patternId": "005",
"defaultVisibility": "PLAIN"
}
],
"roleBasedDecryptionPolicy": [
{
"roles": ["WS_CEMP","WS_DOC_VERIFIER","WS_FIELD_INSPECTOR","WS_APPROVER","WS_CLERK","SW_CEMP","SW_DOC_VERIFIER","SW_FIELD_INSPECTOR","SW_APPROVER","SW_CLERK","PT_APPROVER", "PT_CEMP", "PT_COLLECTION_EMP", "PT_FIELD_INSPECTOR", "PT_DOC_VERIFIER"],
"attributeAccessList": [
{
"attribute": "street",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "doorNo",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "landmark",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
}
]
},
{
"roles": ["REINDEXING_ROLE"],
"attributeAccessList": [
{
"attribute": "street",
"firstLevelVisibility": "ENCRYPTED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "doorNo",
"firstLevelVisibility": "ENCRYPTED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "landmark",
"firstLevelVisibility": "ENCRYPTED",
"secondLevelVisibility": "PLAIN"
}
]
}
]
},
{
"model": "PropertyDecrypDisabled",
"uniqueIdentifier": {
"name": "propertyId",
"jsonPath": "/propertyId"
},
"attributes": [
{
"name": "street",
"jsonPath": "address/street",
"patternId": null,
"defaultVisibility": "PLAIN"
},
{
"name": "doorNo",
"jsonPath": "address/doorNo",
"patternId": null,
"defaultVisibility": "PLAIN"
},
{
"name": "landmark",
"jsonPath": "address/landmark",
"patternId": null,
"defaultVisibility": "PLAIN"
}
],
"roleBasedDecryptionPolicy": []
},
Next, add the following (if not already there) under roleBasedDecryptionPolicy
of User model (already existing):
Copy "roleBasedDecryptionPolicy": [
{
"roles": ["REINDEXING_ROLE"],
"attributeAccessList": [
{
"attribute": "mobileNumber",
"firstLevelVisibility": "ENCRYPTED",
"secondLevelVisibility": "PLAIN"
}
]
},
{
"roles": ["WS_CEMP","WS_DOC_VERIFIER","WS_FIELD_INSPECTOR","WS_APPROVER","WS_CLERK","SW_CEMP","SW_DOC_VERIFIER","SW_FIELD_INSPECTOR","SW_APPROVER","SW_CLERK"
],
"attributeAccessList": [
{
"attribute": "mobileNumber",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "fatherOrHusbandName",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "correspondenceAddress",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "name",
"firstLevelVisibility": "PLAIN",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "emailId",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "permanentAddress",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
},
{
"attribute": "guardian",
"firstLevelVisibility": "MASKED",
"secondLevelVisibility": "PLAIN"
}
]
}
]
Refer to the SecurityPolicy.json file for full reference.
For modules, there is no such hard rule or pattern for naming the model, but it should be related to that service.
ex : 1.) For user service we have two security policy models User which is used when a user tries to search other user data and he/she will get the PII data in masked, plain or encrypted form depending on the visibility sets for an attribute in the MDMS.
And another model is UserSelf which is used when a user tries to search their own data, they get the data as per the configuration set there. For report and searcher config the model name should be similar to the value that we are setting in the field decryptionPathId . ex:- Employee Report . Employee Report Security Model
2.) For Property we have a model Property
which is for Address fields like street, doorNo, landmark encryption and decryption.
For an attribute where its firstLevelVisibility is set as "Masked " and whenever the respective search API is called without the plain Access Request then in the API response for that attribute we will get the masked value
for ex :- if for mobile number attribute’s firstLevelVisibility is masked and its plain value is 9089243280 then in response, we will get value as ******3280 and the masking pattern is defined in the MaskingPattern MDMS file and the pattern is picked up based on patternId. Similarly, if firstLevelVisibility is set as "ENCRYPTED " we will get the encrypted value of that plain data (which is present in DB) in the response.
NOTE: For adding of new attribute for encryption , the following things need to be kept in mind:
We do not have a direct approach to it, but a workaround is as follows:
We need to make sure which property has to be encrypted and what is the path of the property in the Request/Response object of Property. We can add a new property to the existing model Property.
The inclusion of any new attribute here would need encryption of the old data for this new property.
For that, in MDMS, we will have to replace the existing model attributes with only new attributes and hit the _encryptOldData
API. Once old data encryption is done, we can put back all the required attributes (old and +new) in the model.
Also, before starting the encryption of old data, we will have to check the latest record of the table eg_pt_enc_audit.
If the latest record has offset and record count value other than 0 then insert a random record with offset and record count as 0 and createdtime
and encryptiontime
as a current timestamp in millis in utc.
For further info about adding models refer here
Backend Service Changes
Update the pom.xml file.
Upgrade services-common library version.
Copy <dependency>
<groupId>org.egov.services</groupId>
<artifactId>services-common</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
Upgrade the tracer library version.
Copy <dependency>
<groupId>org.egov.services</groupId>
<artifactId>tracer</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
Copy <dependency>
<groupId>org.egov</groupId>
<artifactId>enc-client</artifactId>
<version>2.0.4-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</exclusion>
</exclusions>
</dependency>
Application Properties Changes
In property-services encryption and decryption endpoints should be declared as follows:
Copy #--------enable/disable ABAC in encryption----------#
property.decryption.abac.enabled=true
encryption.batch.value=500
encryption.offset.value=0
#-------Persister topics for oldDataEncryption-------#
property.oldDataEncryptionStatus.topic=pt-enc-audit
persister.update.property.oldData.topic=update-property-encryption
persister.update.property.audit.oldData.topic=update-property-audit-enc
persister.save.property.fuzzy.topic=save-property-fuzzy-data
Now, update the following existing property in application-properties:
property.es.index=pt-fuzzy-search-index
EncryptionDecryptionUtil.java:
We need an interfacing file for handling the encryption and decryption of attributes and for interacting with enc-client library directly.
For reference follow the below code snippet
Copy package org.egov.pt.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.extern.slf4j.Slf4j;
import org.egov.common.contract.request.RequestInfo;
import org.egov.common.contract.request.Role;
import org.egov.common.contract.request.User;
import org.egov.encryption.EncryptionService;
import org.egov.encryption.audit.AuditService;
import org.egov.pt.config.PropertyConfiguration;
import org.egov.tracer.model.CustomException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import java.io.IOException;
import java.util.*;
@Slf4j
@Component
public class EncryptionDecryptionUtil {
private EncryptionService encryptionService;
@Autowired
private PropertyConfiguration config;
@Autowired
private AuditService auditService;
@Autowired
private ObjectMapper objectMapper;
@Value(("${state.level.tenant.id}"))
private String stateLevelTenantId;
@Value(("${property.decryption.abac.enabled}"))
private boolean abacEnabled;
public EncryptionDecryptionUtil(EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
public <T> T encryptObject(Object objectToEncrypt, String key, Class<T> classType) {
try {
if (objectToEncrypt == null) {
return null;
}
T encryptedObject = encryptionService.encryptJson(objectToEncrypt, key, config.getStateLevelTenantId(), classType);
if (encryptedObject == null) {
throw new CustomException("ENCRYPTION_NULL_ERROR", "Null object found on performing encryption");
}
return encryptedObject;
} catch (Exception e) {
log.error("Unknown Error occurred while encrypting", e);
throw new CustomException("UNKNOWN_ERROR", "Unknown error occurred in encryption process");
}
}
public <E, P> P decryptObject(Object objectToDecrypt, String key, Class<E> classType, RequestInfo requestInfo) {
try {
boolean objectToDecryptNotList = false;
if (objectToDecrypt == null) {
return null;
}
if (!(objectToDecrypt instanceof List)) {
objectToDecryptNotList = true;
objectToDecrypt = Collections.singletonList(objectToDecrypt);
}
Map<String, String> keyPurposeMap = getKeyToDecrypt(objectToDecrypt);
String purpose = keyPurposeMap.get("PropertyEncDisabledSearch");
if(key.equalsIgnoreCase(PTConstants.PROPERTY_MODEL))
key = keyPurposeMap.get("key");
P decryptedObject = (P) encryptionService.decryptJson(requestInfo, objectToDecrypt, key, purpose, classType);
if (decryptedObject == null) {
throw new CustomException("DECRYPTION_NULL_ERROR", "Null object found on performing decryption");
}
if (objectToDecryptNotList) {
decryptedObject = (P) ((List<E>) decryptedObject).get(0);
}
return decryptedObject;
} catch (IOException | HttpClientErrorException | HttpServerErrorException | ResourceAccessException e) {
log.error("Error occurred while decrypting", e);
throw new CustomException("DECRYPTION_SERVICE_ERROR", "Error occurred in decryption process");
} catch (Exception e) {
log.error("Unknown Error occurred while decrypting", e);
throw new CustomException("UNKNOWN_ERROR", "Unknown error occurred in decryption process");
}
}
/**
* Setting a fake user Info in case of open API calls
*
* @param requestInfo
* @return
*/
private RequestInfo copyRequestInfoForEncryption(RequestInfo requestInfo) {
RequestInfo requestInfoForDecryption;
User fakeUSerInfo = User.builder().uuid("no uuid").type("EMPLOYEE").build();
if ( requestInfo != null ) {
requestInfoForDecryption = RequestInfo.builder()
.correlationId(requestInfo.getCorrelationId())
.authToken(requestInfo.getAuthToken())
.apiId(requestInfo.getApiId())
.msgId(requestInfo.getMsgId())
.build();
if (requestInfo.getUserInfo() == null)
requestInfoForDecryption.setUserInfo(fakeUSerInfo);
else
requestInfoForDecryption.setUserInfo(requestInfo.getUserInfo());
} else {
requestInfoForDecryption = RequestInfo.builder().
userInfo(fakeUSerInfo)
.build();
}
return requestInfoForDecryption;
}
private User getEncrichedandCopiedUserInfo(User userInfo) {
List<Role> newRoleList = new ArrayList<>();
Boolean isUserTypeRoleFound = false;
List<Role> roles = userInfo.getRoles();
if (!CollectionUtils.isEmpty(roles)) {
for (Role role : roles) {
if (role.getCode().equalsIgnoreCase(userInfo.getType()))
isUserTypeRoleFound = true;
Role newRole = Role.builder()
.tenantId(role.getTenantId())
.code(role.getCode())
.name(role.getName())
.id(role.getId())
.build();
newRoleList.add(newRole);
}
}
if (!isUserTypeRoleFound) {
Role roleFromtype = Role.builder()
.tenantId(userInfo.getTenantId())
.code(userInfo.getType())
.name(userInfo.getType())
.build();
newRoleList.add(roleFromtype);
}
return User.builder()
.roles(newRoleList)