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

Add Permission Validation for Hybrid User Role Deletion #6303

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ private RoleConstants() {
// Role properties
public static final String IS_SHARED_ROLE_PROP_NAME = "isSharedRole";

// NamedPreparedStatement Params
public static final String ROLE_ID = "roleId";
public static final String TENANT_ID = "tenantId";
public static final String PERMITTED_ORG_ID = "permittedOrgId";
public static final String USERNAME = "username";

/**
* Grouping of constants related to database table names.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
import org.wso2.carbon.identity.core.model.FilterTreeBuilder;
import org.wso2.carbon.identity.core.model.Node;
import org.wso2.carbon.identity.core.model.OperationNode;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.organization.management.service.OrganizationManager;
import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementException;
import org.wso2.carbon.identity.organization.management.service.util.OrganizationManagementUtil;
import org.wso2.carbon.identity.organization.management.service.util.Utils;
import org.wso2.carbon.identity.role.v2.mgt.core.dao.RoleDAO;
import org.wso2.carbon.identity.role.v2.mgt.core.dao.RoleMgtDAOFactory;
import org.wso2.carbon.identity.role.v2.mgt.core.exception.IdentityRoleManagementClientException;
Expand Down Expand Up @@ -423,6 +425,8 @@ public List<UserBasicInfo> getUserListOfRole(String roleId, String tenantDomain)
public RoleBasicInfo updateUserListOfRole(String roleId, List<String> newUserIDList, List<String> deletedUserIDList,
String tenantDomain) throws IdentityRoleManagementException {

deletedUserIDList = getEligibleUserIDsForUserRemovalFromRole(roleId, deletedUserIDList, tenantDomain);

List<RoleManagementListener> roleManagementListenerList = RoleManagementServiceComponentHolder.getInstance()
.getRoleManagementListenerList();
for (RoleManagementListener roleManagementListener : roleManagementListenerList) {
Expand Down Expand Up @@ -1245,4 +1249,72 @@ private List<String> getUserNamesByIDs(List<String> userIDs, String tenantDomain

return userIDResolver.getNamesByIDs(userIDs, tenantDomain);
}

/**
* Get user IDs by usernames.
*
* @param userNames List of usernames.
* @param tenantDomain Tenant Domain.
* @return List of user IDs.
* @throws IdentityRoleManagementException IdentityRoleManagementException.
*/
private List<String> getUserIDsByNames(List<String> userNames, String tenantDomain)
throws IdentityRoleManagementException {

return userIDResolver.getIDsByNames(userNames, tenantDomain);
}
sadilchamishka marked this conversation as resolved.
Show resolved Hide resolved

/**
* Updates the list of user IDs intended for deletion based on the specified permissions for the given role
* and permitted organization in the tenant domain.
*
* @param roleId The role ID associated with the users.
* @param deletedUserIDList The list of user IDs intended for deletion.
* @param tenantDomain The tenant domain.
* @param permittedOrgId The ID of the organization permitted for the operation.
* @return A modified list of user IDs that are permitted to be deleted.
* @throws IdentityRoleManagementException If an error occurs while updating the user ID list.
*/
private List<String> updateDeletedUserIDListBasedOnPermission(String roleId, List<String> deletedUserIDList,
String tenantDomain, String permittedOrgId)
throws IdentityRoleManagementException {

List<String> deletedUserNamesList = getUserNamesByIDs(deletedUserIDList, tenantDomain);

List<String> modifiedDeletedUserNamesList =
roleDAO.getEligibleUsernamesForUserRemovalFromRole(roleId, deletedUserNamesList, tenantDomain,
permittedOrgId);

return getUserIDsByNames(modifiedDeletedUserNamesList, tenantDomain);
}

/**
* Retrieves the list of user IDs eligible for removal from the specified role in the given tenant domain,
* based on the permissions of the requesting organization.
*
* @param roleId The role ID from which the users are to be removed.
* @param deletedUserIDList The list of user IDs intended for removal.
* @param tenantDomain The tenant domain where the operation is being performed.
* @return A list of user IDs eligible for removal from the specified role.
* @throws IdentityRoleManagementException If an error occurs while validating permissions or retrieving the
* organization ID.
*/
private List<String> getEligibleUserIDsForUserRemovalFromRole(String roleId, List<String> deletedUserIDList,
String tenantDomain)
throws IdentityRoleManagementException {

int tenantId = IdentityTenantUtil.getTenantId(tenantDomain);
try {
if (OrganizationManagementUtil.isOrganization(tenantId)) {
return updateDeletedUserIDListBasedOnPermission(roleId, deletedUserIDList, tenantDomain,
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
Utils.getOrganizationId());
}
} catch (OrganizationManagementException e) {
String errorMessage = "Error while retrieving the organization id for the given tenantDomain: "
+ tenantDomain;
throw new IdentityRoleManagementServerException(UNEXPECTED_SERVER_ERROR.getCode(), errorMessage, e);
}

return deletedUserIDList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -510,4 +510,26 @@ default boolean isSharedRole(String roleId, String tenantDomain) throws Identity

return false;
}

/**
* Retrieve the list of usernames eligible to be removed from the specified role within the given tenant domain,
* based on the permissions of the requesting organization.
* <p>
* This method ensures that the requesting organization has the necessary permissions to remove the specified users
* from the given role.
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
*
* @param roleId The role ID from which the users are to be removed.
* @param usernamesToBeRemoved The list of usernames intended for removal.
* @param tenantDomain The tenant domain where the operation is being performed.
* @param requestingOrgId The ID of the requesting organization performing the operation.
* @return A list of usernames that the requesting organization is permitted to remove from the given role.
* @throws IdentityRoleManagementException If an error occurs while validating the permissions or retrieving
* eligible usernames.
*/
default List<String> getEligibleUsernamesForUserRemovalFromRole(String roleId, List<String> usernamesToBeRemoved,
String tenantDomain, String requestingOrgId)
throws IdentityRoleManagementException {

throw new NotImplementedException("getEligibleUsernamesForUserRemovalFromRole method is not implemented.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,15 @@
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.MY_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.ORACLE;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.ORGANIZATION;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.PERMITTED_ORG_ID;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.POSTGRE_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.ROLE_ID;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.RoleTableColumns.ROLE_NAME;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.RoleTableColumns.UM_USER_NAME;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.RoleTableColumns.USER_NOT_FOUND_ERROR_MESSAGE;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.SYSTEM;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.TENANT_ID;
import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.USERNAME;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.ADD_APP_ROLE_ASSOCIATION_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.ADD_GROUP_TO_ROLE_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.ADD_GROUP_TO_ROLE_SQL_MSSQL;
Expand Down Expand Up @@ -155,6 +160,10 @@
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_LIMITED_USER_LIST_OF_ROLE_ORACLE;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_LIMITED_USER_LIST_OF_ROLE_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_MAIN_ROLE_TO_SHARED_ROLE_MAPPINGS_BY_SUBORG_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_NOT_RESTRICTED_USERNAMES_BY_ROLE_HEAD;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_NOT_RESTRICTED_USERNAMES_BY_ROLE_TAIL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_RESTRICTED_USERNAMES_BY_ROLE_AND_ORG_HEAD;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_RESTRICTED_USERNAMES_BY_ROLE_AND_ORG_TAIL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_ROLES_BY_APP_ID_SQL;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_ROLES_BY_TENANT_AND_ROLE_NAME_DB2;
import static org.wso2.carbon.identity.role.v2.mgt.core.dao.SQLQueries.GET_ROLES_BY_TENANT_AND_ROLE_NAME_INFORMIX;
Expand Down Expand Up @@ -1287,7 +1296,7 @@ public List<String> getRoleIdListOfUser(String userId, String tenantDomain) thro
try (Connection connection = IdentityDatabaseUtil.getUserDBConnection(false);
NamedPreparedStatement statement = new NamedPreparedStatement(connection, GET_ROLE_ID_LIST_OF_USER_SQL)) {

statement.setString(RoleConstants.RoleTableColumns.UM_USER_NAME, nameWithoutDomain);
statement.setString(UM_USER_NAME, nameWithoutDomain);
statement.setInt(RoleConstants.RoleTableColumns.UM_TENANT_ID, tenantId);
statement.setString(RoleConstants.RoleTableColumns.UM_DOMAIN_NAME, domainName);
try (ResultSet resultSet = statement.executeQuery()) {
Expand Down Expand Up @@ -1732,6 +1741,79 @@ public boolean isSharedRole(String roleId, String tenantDomain) throws IdentityR
return isShared;
}

@Override
public List<String> getEligibleUsernamesForUserRemovalFromRole(String roleId, List<String> deletedUserNamesList,
String tenantDomain, String permittedOrgId)
throws IdentityRoleManagementException {

if (deletedUserNamesList == null || deletedUserNamesList.isEmpty()) {
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
return Collections.emptyList(); // Return early if no usernames are provided
}

int tenantId = IdentityTenantUtil.getTenantId(tenantDomain);
List<String> permittedUserNames = new ArrayList<>();

// Dynamically build placeholders for the IN clause
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
String placeholders = deletedUserNamesList.stream()
.map(username -> ":username" + deletedUserNamesList.indexOf(username))
.collect(Collectors.joining(","));

// Query 1: NOT_RESTRICTED usernames
String query1 =
GET_NOT_RESTRICTED_USERNAMES_BY_ROLE_HEAD + placeholders + GET_NOT_RESTRICTED_USERNAMES_BY_ROLE_TAIL;

// Query 2: RESTRICTED usernames with permitted deletion access
String query2 = GET_RESTRICTED_USERNAMES_BY_ROLE_AND_ORG_HEAD + placeholders +
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
GET_RESTRICTED_USERNAMES_BY_ROLE_AND_ORG_TAIL;

try (Connection connection = IdentityDatabaseUtil.getUserDBConnection(false)) {

// Execute Query 1
try (NamedPreparedStatement ps1 = new NamedPreparedStatement(connection, query1)) {
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
ps1.setString(ROLE_ID, roleId);
ps1.setInt(TENANT_ID, tenantId);

for (int i = 0; i < deletedUserNamesList.size(); i++) {
ps1.setString(USERNAME + i, deletedUserNamesList.get(i));
}

try (ResultSet rs1 = ps1.executeQuery()) {
while (rs1.next()) {
permittedUserNames.add(rs1.getString(UM_USER_NAME));
}
}
}

// Execute Query 2
try (NamedPreparedStatement ps2 = new NamedPreparedStatement(connection, query2)) {
ps2.setString(ROLE_ID, roleId);
ps2.setInt(TENANT_ID, tenantId);
ps2.setString(PERMITTED_ORG_ID, permittedOrgId);

for (int i = 0; i < deletedUserNamesList.size(); i++) {
ps2.setString(USERNAME + i, deletedUserNamesList.get(i));
}

try (ResultSet rs2 = ps2.executeQuery()) {
while (rs2.next()) {
String username = rs2.getString(UM_USER_NAME);
if (!permittedUserNames.contains(username)) {
permittedUserNames.add(username);
}
}
}
}

BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
} catch (SQLException e) {
String errorMessage =
String.format("Error while retrieving permitted usernames for role ID: %s in tenant domain: %s",
roleId, tenantDomain);
throw new IdentityRoleManagementServerException(UNEXPECTED_SERVER_ERROR.getCode(), errorMessage, e);
}

BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved
return permittedUserNames;
}

/**
* Check tenant is a sub organization.
*
Expand Down Expand Up @@ -2247,7 +2329,7 @@ private void processBatchUpdateForUsers(String roleName, int audienceRefId, List
domainName = domainName.toUpperCase(Locale.ENGLISH);
}
String nameWithoutDomain = UserCoreUtil.removeDomainFromName(userName);
statement.setString(RoleConstants.RoleTableColumns.UM_USER_NAME, nameWithoutDomain);
statement.setString(UM_USER_NAME, nameWithoutDomain);
statement.setString(RoleConstants.RoleTableColumns.UM_ROLE_NAME, roleName);
statement.setInt(RoleConstants.RoleTableColumns.UM_TENANT_ID, tenantId);
statement.setInt(RoleConstants.RoleTableColumns.UM_AUDIENCE_REF_ID, audienceRefId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,20 @@ public class SQLQueries {
public static final String INSERT_MAIN_TO_SHARED_ROLE_RELATIONSHIP = "INSERT INTO UM_SHARED_ROLE " +
"(UM_SHARED_ROLE_ID, UM_MAIN_ROLE_ID, UM_SHARED_ROLE_TENANT_ID, UM_MAIN_ROLE_TENANT_ID) " +
"VALUES (:UM_SHARED_ROLE_ID;, :UM_MAIN_ROLE_ID;, :UM_SHARED_ROLE_TENANT_ID;, :UM_MAIN_ROLE_TENANT_ID;)";

public static final String GET_NOT_RESTRICTED_USERNAMES_BY_ROLE_HEAD =
"SELECT UM_USER_NAME FROM UM_HYBRID_USER_ROLE WHERE UM_ROLE_ID = :roleId; AND UM_USER_NAME IN (";
BimsaraBodaragama marked this conversation as resolved.
Show resolved Hide resolved

public static final String GET_NOT_RESTRICTED_USERNAMES_BY_ROLE_TAIL =
"); AND UM_TENANT_ID = :tenantId; AND UM_EDIT_RESTRICTION = 'NOT_RESTRICTED';";

public static final String GET_RESTRICTED_USERNAMES_BY_ROLE_AND_ORG_HEAD =
"SELECT r.UM_USER_NAME FROM UM_HYBRID_USER_ROLE r "
+ "INNER JOIN UM_HYBRID_USER_ROLE_RESTRICTED_EDIT_PERMISSIONS p "
+ "ON r.UM_ID = p.UM_HYBRID_USER_ROLE_ID AND r.UM_TENANT_ID = p.UM_HYBRID_USER_ROLE_TENANT_ID "
+ "WHERE r.UM_ROLE_ID = :roleId; AND r.UM_USER_NAME IN (";

public static final String GET_RESTRICTED_USERNAMES_BY_ROLE_AND_ORG_TAIL =
"); AND r.UM_TENANT_ID = :tenantId; AND p.UM_PERMITTED_ORG_ID = :permittedOrgId; " +
"AND r.UM_EDIT_OPERATION = 'DELETE'; AND r.UM_EDIT_RESTRICTION = 'RESTRICTED';";
}
Loading