Skip to content

Commit

Permalink
Issue #LR-676 merge: User Delete - ownership transfer api (#1236)
Browse files Browse the repository at this point in the history
  • Loading branch information
BharathwajShankar authored Apr 1, 2024
1 parent a26e9ce commit 2dd227b
Show file tree
Hide file tree
Showing 12 changed files with 593 additions and 5 deletions.
8 changes: 8 additions & 0 deletions controller/app/controllers/usermanagement/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public class UserController extends BaseController {
@Inject
@Named("user_self_declaration_management_actor")
private ActorRef userSelfDeclarationManagementActor;
@Inject
@Named("user_ownership_transfer_actor")
private ActorRef userOwnershipTransferActor;

public CompletionStage<Result> createUser(Http.Request httpRequest) {
return handleRequest(
Expand Down Expand Up @@ -472,4 +475,9 @@ public CompletionStage<Result> updateUserDeclarations(Http.Request httpRequest)
true,
httpRequest);
}
public CompletionStage<Result> ownershipTransferUser(Http.Request httpRequest) {
return handleRequest(userOwnershipTransferActor,
ActorOperations.USER_OWNERSHIP_TRANSFER.getValue(),
httpRequest.body().asJson(), httpRequest);
}
}
3 changes: 2 additions & 1 deletion controller/app/util/ACTORS.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public enum ACTORS {
USER_UPDATE_ACTOR(UserUpdateActor.class, "user_update_actor"),
BACKGROUND_JOB_MANAGER_ACTOR(BackgroundJobManager.class, "background_job_manager_actor"),
USER_DELETION_BACKGROUND_JOB_ACTOR(
UserDeletionBackgroundJobActor.class, "user_deletion_background_job_actor");
UserDeletionBackgroundJobActor.class, "user_deletion_background_job_actor"),
USER_OWNERSHIP_TRANSFER_ACTOR(UserOwnershipTransferActor.class,"user_ownership_transfer_actor");

ACTORS(Class clazz, String name) {
actorClass = clazz;
Expand Down
10 changes: 10 additions & 0 deletions controller/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ akka {
{
dispatcher = akka.actor.brr-usr-dispatcher
}
"/user_ownership_transfer_actor"
{
router = smallest-mailbox-pool
nr-of-instances = 5
dispatcher = brr-usr-dispatcher
}
"/user_ownership_transfer_actor/*"
{
dispatcher = akka.actor.brr-usr-dispatcher
}
"/background_job_manager_actor"
{
router = smallest-mailbox-pool
Expand Down
2 changes: 1 addition & 1 deletion controller/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,4 @@ POST /v1/system/settings/set @controllers.systemsettings.Syst


POST /v1/user/delete @controllers.usermanagement.UserStatusController.deleteUser(request: play.mvc.Http.Request)

POST /v1/user/ownership/transfer @controllers.usermanagement.UserController.ownershipTransferUser(request: play.mvc.Http.Request)
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ public enum ResponseCode {
ResponseMessage.Message.INVALID_TENANT_SECURITY_LEVEL_LOWER),
cannotDeleteUser(
ResponseMessage.Key.CANNOT_DELETE_USER, ResponseMessage.Message.CANNOT_DELETE_USER),

cannotTransferOwnership(
ResponseMessage.Key.CANNOT_TRANSFER_OWNERSHIP, ResponseMessage.Message.CANNOT_TRANSFER_OWNERSHIP),
OK(200),
SUCCESS(200),
CLIENT_ERROR(400),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ interface Message {
String INVALID_TENANT_SECURITY_LEVEL_LOWER =
"Tenant level's security {0} cannot be lower than system level's security {1}. Please provide a valid data security level.";
String CANNOT_DELETE_USER = "User is restricted from deleting account based on roles!";
String CANNOT_TRANSFER_OWNERSHIP = "User is restricted from transfering the ownership based on roles!";
}

interface Key {
Expand Down Expand Up @@ -208,5 +209,6 @@ interface Key {
String MISSING_DEFAULT_SECURITY_LEVEL = "0081";
String INVALID_TENANT_SECURITY_LEVEL_LOWER = "0082";
String CANNOT_DELETE_USER = "0083";
String CANNOT_TRANSFER_OWNERSHIP = "0084";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,14 @@ public final class JsonKey {
public static final String USER_TABLE_STATUS = "userTable";
public static final String SUGGESTED_USERS = "suggested_users";
public static final String DELETE_USER_ACTON = "delete-user";
public static final String ACTION_BY = "actionBy";
public static final String FROM_USER = "fromUser";
public static final String TO_USER = "toUser";
public static final String USER_OWNERSHIP_TRANSFER_ACTION = "ownership-transfer";
public static final String FROM_USER_PROFILE = "fromUserProfile";
public static final String TO_USER_PROFILE = "toUserProfile";
public static final String ASSET_INFORMATION = "assetInformation";
public static final String USER_OWNERSHIP_TRANSFER_TOPIC = "user-ownership-transfer-topic";
public static final String OBJECT = "object";
public static final String EDATA = "edata";
public static final String MANAGED_USERS = "managed_users";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ public enum ActorOperations {
DELETE_LOCATION_FROM_ES("deleteLocationDataFromES", "LBKGDEL"),
ADD_ENCRYPTION_KEY("addEncryptionKey", "ADENCKEY"),
USER_CURRENT_LOGIN("userCurrentLogin", "USRLOG"),
DELETE_USER("deleteUser", "USRDLT");
DELETE_USER("deleteUser", "USRDLT"),
USER_OWNERSHIP_TRANSFER("userOwnershipTransfer","UOWNTRANS");

private String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,6 @@ sunbird_password_reset_login_page_url=/resources
isFormValidationRequired=true
userProfileConfigMap={\"type\":\"profileconfig\",\"subtype\":\"28\",\"action\":\"get\",\"component\":\"*\",\"framework\":\"*\",\"data\":{\"templateName\":\"profileConfig_v2\",\"action\":\"get\",\"fields\":[{\"code\":\"persona\",\"children\":{\"administrator\":[{\"code\":\"district\"},{\"code\":\"state\"},{\"code\":\"subPersona\",\"type\":\"select\",\"default\":null,\"templateOptions\":{\"options\":[{\"label\":\"Headmaster\",\"value\":\"hm\"},{\"label\":\"Cluster Resource Person\",\"value\":\"crp\"}]}},{\"code\":\"block\"},{\"code\":\"cluster\"},{\"code\":\"school\"}],\"teacher\":[{\"code\":\"state\"},{\"code\":\"district\"},{\"code\":\"block\"},{\"code\":\"cluster\"},{\"code\":\"school\"}],\"student\":[{\"code\":\"state\"},{\"code\":\"district\"},{\"code\":\"block\"},{\"code\":\"cluster\"},{\"code\":\"school\"}],\"parent\":[{\"code\":\"state\"},{\"code\":\"district\"},{\"code\":\"block\"},{\"code\":\"cluster\"},{\"code\":\"school\"}],\"other\":[{\"code\":\"state\"},{\"code\":\"district\"},{\"code\":\"subPersona\",\"templateOptions\":{\"options\":[{\"value\":\"Doctor (Allopathy)\",\"label\":\"Doctor (Allopathy)\"},{\"value\":\"AYUSH Professional\",\"label\":\"AYUSH Professional\"}]}},{\"code\":\"block\"},{\"code\":\"cluster\"},{\"code\":\"school\"}]}}]},\"created_on\":\"2022-02-10T14:16:51.852Z\",\"last_modified_on\":\"2022-11-14T05:45:02.685Z\",\"rootOrgId\":\"*\"}
sunbird_userorg_keyspace=sunbird

user-ownership-transfer-topic={{env_name}}.user.ownership.transfer
user-deletion-roles=public
user-deletion-broadcast-topic={{env_name}}.delete.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package org.sunbird.actor.user;

import org.apache.commons.lang.StringUtils;
import org.sunbird.actor.core.BaseActor;
import org.sunbird.exception.ProjectCommonException;
import org.sunbird.exception.ResponseCode;
import org.sunbird.kafka.InstructionEventGenerator;
import org.sunbird.keys.JsonKey;
import org.sunbird.request.Request;
import org.sunbird.request.RequestContext;
import org.sunbird.response.Response;
import org.sunbird.response.ResponseParams;
import org.sunbird.service.user.UserRoleService;
import org.sunbird.service.user.UserService;
import org.sunbird.service.user.impl.UserRoleServiceImpl;
import org.sunbird.service.user.impl.UserServiceImpl;
import org.sunbird.util.ProjectUtil;
import org.sunbird.util.PropertiesCache;

import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;

import static org.sunbird.validator.orgvalidator.BaseOrgRequestValidator.ERROR_CODE;

public class UserOwnershipTransferActor extends BaseActor {

private final UserRoleService userRoleService = UserRoleServiceImpl.getInstance();
private final UserService userService = UserServiceImpl.getInstance();

@Override
public void onReceive(Request request) throws Throwable {
handleOwnershipTransfer(request);
}

private void handleOwnershipTransfer(Request request) {
validateUserDetails(request.getRequest(), request.getRequestContext());
String userId = (String) ((Map<String, Object>) request.getRequest().get(JsonKey.ACTION_BY))
.get(JsonKey.USER_ID);
validateActionByUserRole(userId, request);
List<Map<String, Object>> objects = getObjectsFromRequest(request);
if (!objects.isEmpty()) {
objects.forEach(object -> sendInstructionEvent(request, object));
} else {
sendInstructionEvent(request, Collections.emptyMap());
}
Response response = sendResponse("Ownership transfer process is submitted successfully!");
sender().tell(response, self());
}

private void validateUserDetails(Map<String, Object> data, RequestContext requestContext) {
validateAndProceed(data, JsonKey.ACTION_BY, requestContext);
validateAndProceed(data, JsonKey.FROM_USER, requestContext);
validateAndProceed(data, JsonKey.TO_USER, requestContext);
}

private void validateAndProceed(Map<String, Object> data, String key, RequestContext requestContext) {
if (data.containsKey(key)) {
validateUser(data.get(key), key, requestContext, data);
} else {
throwInvalidRequestDataException(key + " key is not present in the data.");
}
}

private void validateUser(Object userNode, String userLabel, RequestContext requestContext,
Map<String, Object> data) {
if (userNode instanceof Map) {
Map<String, Object> user = (Map<String, Object>) userNode;
String userId = StringUtils.trimToNull(Objects.toString(user.get(JsonKey.USER_ID), ""));
String userName = StringUtils.trimToNull(Objects.toString(user.get(JsonKey.USERNAME), ""));

if (StringUtils.isBlank(StringUtils.trimToNull(userId)) ||
StringUtils.isBlank(StringUtils.trimToNull(userName))) {
throwInvalidRequestDataException("User id / user name key is not present in the " + userLabel);
}

if (validUser(userId, requestContext)) {
validateAndFilterRoles(user, userLabel, data);
} else {
throwClientErrorException();
}
}
}

private void validateAndFilterRoles(Map<String, Object> user, String userLabel, Map<String, Object> data) {
if (!userLabel.equals(JsonKey.ACTION_BY) && !user.containsKey(JsonKey.ROLES)) {
throwInvalidRequestDataException("Roles key is not present for " + userLabel);
}
if (user.containsKey(JsonKey.ROLES)) {
Object roles = user.get(JsonKey.ROLES);
if (roles instanceof List) {
List<?> rolesList = (List<?>) roles;
if (rolesList.isEmpty()) {
throwInvalidRequestDataException("Roles are empty in " + userLabel + " details.");
} else {
List<String> filteredRoles = filterRolesByOrganisationId((List<?>) roles,
(String) data.get(JsonKey.ORGANISATION_ID));
user.put(JsonKey.ROLES, filteredRoles);
}
} else {
throwDataTypeErrorException();
}
}
}

private List<String> filterRolesByOrganisationId(List<?> roles, String targetOrganisationId) {
List<String> filteredRoles = new ArrayList<>();
for (Object role : roles) {
if (role instanceof Map) {
Map<String, Object> roleMap = (Map<String, Object>) role;
List<Map<String, Object>> scopeList = (List<Map<String, Object>>) roleMap.get("scope");
if (scopeList != null && !scopeList.isEmpty()) {
for (Map<String, Object> scope : scopeList) {
String organisationId = (String) scope.get("organisationId");
if (targetOrganisationId.equals(organisationId)) {
filteredRoles.add((String) roleMap.get("role"));
break;
}
}
}
}
}
return filteredRoles;
}

private void throwInvalidRequestDataException(String message) {
throw new ProjectCommonException(
ResponseCode.invalidRequestData,
message,
ResponseCode.CLIENT_ERROR.getResponseCode());
}

private void throwClientErrorException() {
ProjectCommonException.throwClientErrorException(
ResponseCode.invalidParameter,
MessageFormat.format(ResponseCode.invalidParameter.getErrorMessage(), JsonKey.USER_ID));
}

private void throwDataTypeErrorException() {
throw new ProjectCommonException(
ResponseCode.dataTypeError,
ProjectUtil.formatMessage(
ResponseCode.dataTypeError.getErrorMessage(), JsonKey.ROLES, JsonKey.LIST),
ERROR_CODE);
}

private boolean validUser(String userId, RequestContext context) {
return StringUtils.isNotBlank(userId) && userExists(userId, context);
}

private boolean userExists(String userId, RequestContext context) {
try {
userService.getUserById(userId, context);
return true;
} catch (Exception ex) {
return false;
}
}

private void validateActionByUserRole(String userId, Request request) {
List<Map<String, Object>> userRoles = userRoleService.getUserRoles(userId, request.getRequestContext());
boolean hasOrgAdminRole = userRoles.stream().anyMatch(role -> JsonKey.ORG_ADMIN.equals(role.get(JsonKey.ROLE)));
if (!hasOrgAdminRole) {
throw new ProjectCommonException(
ResponseCode.cannotTransferOwnership,
ResponseCode.cannotTransferOwnership.getErrorMessage(),
ResponseCode.CLIENT_ERROR.getResponseCode());
}
}

private List<Map<String, Object>> getObjectsFromRequest(Request request) {
return Optional.ofNullable((List<Map<String, Object>>) request.getRequest().get("objects"))
.orElse(Collections.emptyList());
}

private void sendInstructionEvent(Request request, Map<String, Object> object) {
Map<String, Object> data = prepareEventData(request, object);
CompletableFuture.runAsync(() -> {
try {
PropertiesCache propertiesCache = PropertiesCache.getInstance();
InstructionEventGenerator.pushInstructionEvent(propertiesCache.getProperty(JsonKey.USER_OWNERSHIP_TRANSFER_TOPIC), data);
} catch (Exception e) {
logger.error("Error pushing to instruction event", e);
}
});
}

private Map<String, Object> prepareEventData(Request request, Map<String, Object> object) {
Map<String, Object> actor = Map.of("id", "ownership-transfer", "type", "System");
Map<String, Object> edataBase = Map.of(
JsonKey.ACTION, JsonKey.USER_OWNERSHIP_TRANSFER_ACTION,
JsonKey.ORGANISATION_ID, request.getRequest().get(JsonKey.ORGANISATION_ID),
JsonKey.CONTEXT, request.getRequest().get(JsonKey.CONTEXT),
JsonKey.ACTION_BY, request.getRequest().get(JsonKey.ACTION_BY),
JsonKey.FROM_USER_PROFILE, request.getRequest().get(JsonKey.FROM_USER),
JsonKey.TO_USER_PROFILE, request.getRequest().get(JsonKey.TO_USER)
);
Map<String, Object> edata = new HashMap<>(edataBase);
Map<String, Object> assetInformation = new HashMap<>(object);
edata.put(JsonKey.ASSET_INFORMATION, assetInformation);

Map<String, Object> result = new HashMap<>();
Map<String, Object> fromUserDetails = (Map<String, Object>) request.getRequest().get(JsonKey.FROM_USER);
Map<String, Object> objectDetails = Map.of(JsonKey.ID, fromUserDetails.get(JsonKey.USER_ID), JsonKey.TYPE,
JsonKey.USER);
result.put("actor", actor);
result.put(JsonKey.OBJECT, objectDetails);
result.put(JsonKey.EDATA, edata);
return result;
}

Response sendResponse(String statusMessage) {
Response response = new Response();
response.setId("api.user.ownership.transfer");
response.setVer("1.0");
response.setTs(String.valueOf(Calendar.getInstance().getTime().getTime()));
ResponseParams params = new ResponseParams();
params.setResmsgid(UUID.randomUUID().toString());
params.setStatus(String.valueOf(ResponseParams.StatusType.SUCCESSFUL));
response.setParams(params);
response.setResponseCode(ResponseCode.OK);
Map<String, Object> result = Map.of(JsonKey.STATUS, statusMessage);
response.putAll(result);
return response;
}
}
Loading

0 comments on commit 2dd227b

Please sign in to comment.