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 support for claim-wise uniqueness validation #6113

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -37,6 +37,8 @@
import java.util.List;
import java.util.Map;

import static org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimMetadataUtils.getServerLevelClaimUniquenessScope;

/**
* Data access object for org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim.
*/
Expand Down Expand Up @@ -65,8 +67,13 @@ public List<LocalClaim> getLocalClaims(int tenantId) throws ClaimMetadataExcepti

List<AttributeMapping> attributeMappingsOfClaim = claimAttributeMappingsOfDialect.get(claimId);
Map<String, String> propertiesOfClaim = claimPropertiesOfDialect.get(claimId);

localClaims.add(new LocalClaim(claim.getClaimURI(), attributeMappingsOfClaim, propertiesOfClaim));
LocalClaim localClaim = new LocalClaim(claim.getClaimURI(), attributeMappingsOfClaim, propertiesOfClaim);
if (shouldAddUniquenessScopeInClaimProperties(propertiesOfClaim)) {
ClaimConstants.ClaimUniquenessScope uniquenessScope = getServerLevelClaimUniquenessScope();
addUniquenessScopeToClaimProperties(localClaim,uniquenessScope);
storeUniquenessScopeInClaimProperties(connection, claimId, uniquenessScope, tenantId);
}
localClaims.add(localClaim);
}
} finally {
IdentityDatabaseUtil.closeConnection(connection);
Expand Down Expand Up @@ -179,6 +186,10 @@ public void addLocalClaim(LocalClaim localClaim, int tenantId) throws ClaimMetad
}

addClaimAttributeMappings(connection, localClaimId, localClaim.getMappedAttributes(), tenantId);
if (shouldAddUniquenessScopeInClaimProperties(localClaim.getClaimProperties())) {
ClaimConstants.ClaimUniquenessScope uniquenessScope = getServerLevelClaimUniquenessScope();
addUniquenessScopeToClaimProperties(localClaim,uniquenessScope);
}
addClaimProperties(connection, localClaimId, localClaim.getClaimProperties(), tenantId);

// End transaction
Expand Down Expand Up @@ -209,6 +220,10 @@ public void updateLocalClaim(LocalClaim localClaim, int tenantId) throws ClaimMe
addClaimAttributeMappings(connection, localClaimId, localClaim.getMappedAttributes(), tenantId);

deleteClaimProperties(connection, localClaimId, tenantId);
if (shouldAddUniquenessScopeInClaimProperties(localClaim.getClaimProperties())) {
ClaimConstants.ClaimUniquenessScope uniquenessScope = getServerLevelClaimUniquenessScope();
addUniquenessScopeToClaimProperties(localClaim,uniquenessScope);
}
addClaimProperties(connection, localClaimId, localClaim.getClaimProperties(), tenantId);

// End transaction
Expand Down Expand Up @@ -398,4 +413,47 @@ public List<Claim> fetchMappedExternalClaims(String localClaimURI, int tenantId)
throw new ClaimMetadataException("Error while obtaining mapped external claims for local claim.", e);
}
}

/**
* Checks if the uniqueness scope should be included in the given claim properties.
*
* @param claimProperties Map of claim properties to check.
* @return true if uniqueness scope should be included, false otherwise.
*/
private boolean shouldAddUniquenessScopeInClaimProperties(Map<String, String> claimProperties) {

return claimProperties != null &&
Boolean.parseBoolean(claimProperties.get(ClaimConstants.IS_UNIQUE_CLAIM_PROPERTY)) &&
!claimProperties.containsKey(ClaimConstants.CLAIM_UNIQUENESS_SCOPE_PROPERTY);
}

/**
* Adds the specified uniqueness scope to the claim properties of a given LocalClaim.
*
* @param localClaim LocalClaim instance to which the uniqueness scope property will be added.
* @param uniquenessScope Enum value representing the uniqueness scope to be added.
*/
private void addUniquenessScopeToClaimProperties(LocalClaim localClaim,
ClaimConstants.ClaimUniquenessScope uniquenessScope) {

localClaim.getClaimProperties().put(ClaimConstants.CLAIM_UNIQUENESS_SCOPE_PROPERTY, uniquenessScope.toString());
}

/**
* Stores the uniqueness scope in the claim properties for a given claim in the database.
*
* @param connection Database connection.
* @param claimId ID of the claim for which the uniqueness scope will be stored.
* @param uniquenessScope Enum value representing the uniqueness scope to store.
* @param tenantId ID of the tenant to which the claim belongs.
* @throws ClaimMetadataException if an error occurs while storing the claim properties.
*/
private void storeUniquenessScopeInClaimProperties(Connection connection, int claimId,
ClaimConstants.ClaimUniquenessScope uniquenessScope,
int tenantId) throws ClaimMetadataException {

Map<String, String> uniquenessScopeProperty = new HashMap<>();
uniquenessScopeProperty.put(ClaimConstants.CLAIM_UNIQUENESS_SCOPE_PROPERTY, uniquenessScope.toString());
addClaimProperties(connection, claimId, uniquenessScopeProperty, tenantId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class ClaimConstants {
public static final String READ_ONLY_PROPERTY = "ReadOnly";
public static final String CLAIM_URI_PROPERTY = "ClaimURI";
public static final String MASKING_REGULAR_EXPRESSION_PROPERTY = "MaskingRegEx";
public static final String CLAIM_UNIQUENESS_SCOPE_PROPERTY = "UniquenessScope";
public static final String IS_UNIQUE_CLAIM_PROPERTY = "isUnique";
public static final String UNIQUENESS_VALIDATION_SCOPE = "UserClaimUpdate.UniquenessValidation.ScopeWithinUserstore";

public static final String DEFAULT_ATTRIBUTE = "DefaultAttribute";
public static final String MAPPED_LOCAL_CLAIM_PROPERTY = "MappedLocalClaim";
Expand Down Expand Up @@ -112,4 +115,13 @@ public String getMessage() {
return message;
}
}

/**
* Enum for claim uniqueness validation scopes.
*/
public enum ClaimUniquenessScope {
NONE,
WITHIN_USERSTORE,
ACROSS_USERSTORES
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.wso2.carbon.identity.claim.metadata.mgt.model.ClaimDialect;
import org.wso2.carbon.identity.claim.metadata.mgt.model.ExternalClaim;
import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.core.UserCoreConstants;
Expand All @@ -38,6 +39,8 @@
import java.util.List;
import java.util.Map;

import static org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants.UNIQUENESS_VALIDATION_SCOPE;

/**
* Utility class containing various claim metadata implementation related functionality.
*/
Expand Down Expand Up @@ -318,4 +321,19 @@ public static ClaimMapping convertExternalClaimToClaimMapping(ExternalClaim exte
claimMapping.getClaim().setClaimUri(externalClaim.getClaimURI());
return claimMapping;
}

/**
* Retrieves the server-level uniqueness validation scope for claims based on configuration.
*
* @return Enum value of ClaimConstants.ClaimUniquenessScope indicating the server-level uniqueness scope.
* Returns WITHIN_USERSTORE if the configuration is set to restrict uniqueness within the user store;
* otherwise, returns ACROSS_USERSTORES.
*/
public static ClaimConstants.ClaimUniquenessScope getServerLevelClaimUniquenessScope() {

boolean isScopeWithinUserstore = Boolean.parseBoolean(IdentityUtil.getProperty(UNIQUENESS_VALIDATION_SCOPE));

return isScopeWithinUserstore ? ClaimConstants.ClaimUniquenessScope.WITHIN_USERSTORE :
ClaimConstants.ClaimUniquenessScope.ACROSS_USERSTORES;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException;
import org.wso2.carbon.identity.claim.metadata.mgt.model.LocalClaim;
import org.wso2.carbon.identity.claim.metadata.mgt.util.ClaimConstants;
import org.wso2.carbon.identity.core.AbstractIdentityUserOperationEventListener;
import org.wso2.carbon.identity.core.model.IdentityEventListenerConfig;
import org.wso2.carbon.identity.core.util.IdentityCoreConstants;
Expand Down Expand Up @@ -102,8 +103,9 @@ public boolean doPreSetUserClaimValue(String userName, String claimURI, String c
}
try {
String tenantDomain = getTenantDomain(userStoreManager);
if (isUniqueClaim(claimURI, tenantDomain)) {
return !isClaimDuplicated(userName, claimURI, claimValue, profile, userStoreManager);
ClaimConstants.ClaimUniquenessScope uniquenessScope = getClaimUniquenessScope(claimURI, tenantDomain);
if (shouldValidateUniqueness(uniquenessScope)) {
return !isClaimDuplicated(userName, claimURI, claimValue, profile, userStoreManager, uniquenessScope);
}
} catch (org.wso2.carbon.user.api.UserStoreException | ClaimMetadataException e) {
log.error("Error while retrieving details. " + e.getMessage(), e);
Expand Down Expand Up @@ -132,7 +134,9 @@ private void checkClaimUniqueness(String username, Map<String, String> claims, S
Claim claimObject = null;
for (Map.Entry<String, String> claim : claims.entrySet()) {
try {
if (StringUtils.isNotEmpty(claim.getValue()) && isUniqueClaim(claim.getKey(), tenantDomain)) {
ClaimConstants.ClaimUniquenessScope uniquenessScope =
getClaimUniquenessScope(claim.getKey(), tenantDomain);
if (StringUtils.isNotEmpty(claim.getValue()) && shouldValidateUniqueness(uniquenessScope)) {
try {
claimObject = userStoreManager.getClaimManager().getClaim(claim.getKey());
} catch (org.wso2.carbon.user.api.UserStoreException e) {
Expand All @@ -147,7 +151,8 @@ private void checkClaimUniqueness(String username, Map<String, String> claims, S
claimObject.getDisplayTag() + "!";
throw new UserStoreException(errorMessage, new PolicyViolationException(errorMessage));
}
if (isClaimDuplicated(username, claim.getKey(), claim.getValue(), profile, userStoreManager)) {
if (isClaimDuplicated(username, claim.getKey(), claim.getValue(), profile, userStoreManager,
uniquenessScope)) {
String displayTag = claimObject.getDisplayTag();
if (StringUtils.isBlank(displayTag)) {
displayTag = claim.getKey();
Expand Down Expand Up @@ -175,14 +180,15 @@ private void checkClaimUniqueness(String username, Map<String, String> claims, S
}

private boolean isClaimDuplicated(String username, String claimUri, String claimValue, String profile,
UserStoreManager userStoreManager) throws UserStoreException {
UserStoreManager userStoreManager,
ClaimConstants.ClaimUniquenessScope uniquenessScope) throws UserStoreException {

String domainName = userStoreManager.getRealmConfiguration().getUserStoreProperty(
UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME);
String[] userList;
// Get UserStoreManager from realm since the received one might be for a secondary user store
UserStoreManager userStoreMgrFromRealm = getUserstoreManager(userStoreManager.getTenantId());
if (isScopeWithinUserstore()) {
if (ClaimConstants.ClaimUniquenessScope.WITHIN_USERSTORE.equals(uniquenessScope)) {
String claimValueWithDomain = domainName + UserCoreConstants.DOMAIN_SEPARATOR + claimValue;
userList = userStoreMgrFromRealm.getUserList(claimUri, claimValueWithDomain, profile);
} else {
Expand All @@ -200,17 +206,67 @@ private boolean isClaimDuplicated(String username, String claimUri, String claim
return true;
}

public boolean isUniqueClaim(String claimUrI, String tenantDomain) throws ClaimMetadataException {
/**
* Determines the uniqueness validation scope for a given claim URI.
* This method checks the claim properties to determine how uniqueness should be enforced:
* 1. First checks for explicit uniquenessScope property
* 2. If not found, checks for legacy isUnique property
* 3. If claim is unique, scope is determined by isScopeWithinUserstore server-level configuration
* 4. Defaults to NONE if no uniqueness requirements are found
*
* @param claimUri The URI of the claim to check
* @param tenantDomain The tenant domain where the claim exists
* @return The ClaimUniquenessScope (NONE, WITHIN_USERSTORE, or ACROSS_USERSTORES)
* @throws ClaimMetadataException If there is an error accessing claim metadata
*/
private ClaimConstants.ClaimUniquenessScope getClaimUniquenessScope(String claimUri, String tenantDomain)
throws ClaimMetadataException {

List<LocalClaim> localClaims = UniqueClaimUserOperationDataHolder.getInstance()
.getClaimMetadataManagementService().getLocalClaims(tenantDomain);

LocalClaim targetLocalClaim = localClaims.stream()
.filter(claim -> claim.getClaimURI().equals(claimUri))
.findFirst()
.orElse(null);

if (targetLocalClaim != null) {
String uniquenessScope = targetLocalClaim.getClaimProperty(ClaimConstants.CLAIM_UNIQUENESS_SCOPE_PROPERTY);
if (StringUtils.isNotBlank(uniquenessScope)) {
try {
return ClaimConstants.ClaimUniquenessScope.valueOf(uniquenessScope);
} catch (IllegalArgumentException e) {
if (log.isWarnEnabled()) {
log.warn("Invalid uniqueness validation scope '" + uniquenessScope + "' provided for " +
"claim URI: " + claimUri + ". Defaulting to NONE, where no uniqueness validation " +
"will be performed.");
}
return ClaimConstants.ClaimUniquenessScope.NONE;
}
}

List<LocalClaim> localClaims = UniqueClaimUserOperationDataHolder.getInstance().
getClaimMetadataManagementService().getLocalClaims(tenantDomain);
for (LocalClaim localClaim : localClaims) {
if (localClaim.getClaimURI().equals(claimUrI) &&
Boolean.parseBoolean(localClaim.getClaimProperty(IS_UNIQUE_CLAIM))) {
return true;
boolean isUniqueClaim = Boolean.parseBoolean(targetLocalClaim.getClaimProperty(IS_UNIQUE_CLAIM));
if (isUniqueClaim) {
return isScopeWithinUserstore()
? ClaimConstants.ClaimUniquenessScope.WITHIN_USERSTORE
: ClaimConstants.ClaimUniquenessScope.ACROSS_USERSTORES;
}
}
return false;

return ClaimConstants.ClaimUniquenessScope.NONE;
}

/**
* Determines whether uniqueness validation should be performed for a given uniqueness scope.
* Returns true for any scope other than NONE.
*
* @param uniquenessScope The ClaimUniquenessScope to check
* @return true if uniqueness validation should be performed, false otherwise
* @throws ClaimMetadataException If there is an error processing the metadata
*/
private boolean shouldValidateUniqueness(ClaimConstants.ClaimUniquenessScope uniquenessScope){

return !ClaimConstants.ClaimUniquenessScope.NONE.equals(uniquenessScope);
}

private void checkUsernameUniqueness(String username, UserStoreManager userStoreManager) throws UserStoreException {
Expand All @@ -219,8 +275,9 @@ private void checkUsernameUniqueness(String username, UserStoreManager userStore
String tenantDomain = getTenantDomain(userStoreManager);

try {
if (isUniqueClaim(USERNAME_CLAIM, tenantDomain) &&
isClaimDuplicated(username, USERNAME_CLAIM, username, null, userStoreManager)) {
ClaimConstants.ClaimUniquenessScope uniquenessScope = getClaimUniquenessScope(USERNAME_CLAIM, tenantDomain);
if (shouldValidateUniqueness(uniquenessScope) &&
isClaimDuplicated(username, USERNAME_CLAIM, username, null, userStoreManager, uniquenessScope)) {

errorMessage = "Username " + username + " is already in use by a different user!";
throw new UserStoreException(errorMessage, new PolicyViolationException(errorMessage));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,9 @@
<!-- When updating the claim value, it can be validated against the provided regex pattern.
To enable this option,'EnableUserClaimInputRegexValidation' should be set to true. -->
<!--<EnableUserClaimInputRegexValidation>false</EnableUserClaimInputRegexValidation>-->
<UniquenessValidation>
<ScopeWithinUserstore>false</ScopeWithinUserstore>
</UniquenessValidation>
</UserClaimUpdate>

<AccountSuspension>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,15 @@

<!-- Enable support for storing multiple email addresses and mobile numbers per user. -->
<EnableMultipleEmailsAndMobileNumbers>{{identity_mgt.user_claim_update.enable_multiple_emails_and_mobile_numbers}}</EnableMultipleEmailsAndMobileNumbers>

<!--
Defines the scope for which claim uniqueness validation should happen, this can be either within userstore
or across userstore. This config is already defined in the EventListener with
id="unique_claim_user_operation_event_listener" of type "UserOperationEventListener".
-->
<UniquenessValidation>
<ScopeWithinUserstore>{{identity_mgt.user_claim_update.uniqueness.scope_within_userstore}}</ScopeWithinUserstore>
</UniquenessValidation>
</UserClaimUpdate>

<AccountSuspension>
Expand Down
Loading