Skip to content

Commit

Permalink
Add support for claim-wise uniqueness validation
Browse files Browse the repository at this point in the history
  • Loading branch information
AfraHussaindeen committed Nov 8, 2024
1 parent c0ba5cc commit 89624e0
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 18 deletions.
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

0 comments on commit 89624e0

Please sign in to comment.