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

Check claim uniqueness without duplicate claim check #6492

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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 @@ -127,56 +127,149 @@ public boolean doPreSetUserClaimValues(String userName, Map<String, String> clai
return true;
}

/**
* Validates that user claims are unique and do not conflict with existing users' attributes.
* Also ensures that the password is not used as an attribute value.
*
* @param username The username of the user whose claims are being validated.
* @param claims A map of claim URIs and their respective values to be validated.
* @param profile The profile name associated with the claims.
* @param userStoreManager The user store manager responsible for handling user attributes.
* @param credential The user's password or authentication credential.
* @throws UserStoreException If a claim value is not unique or if the password matches a claim value.
*/
private void checkClaimUniqueness(String username, Map<String, String> claims, String profile,
UserStoreManager userStoreManager, Object credential) throws UserStoreException {

String errorMessage = StringUtils.EMPTY;
List<String> duplicateClaims = new ArrayList<>();
processClaims(username, claims, profile, userStoreManager, credential, true, duplicateClaims);

if (!duplicateClaims.isEmpty()) {
throwDuplicateClaimException(duplicateClaims);
}
}

/**
* Ensures that the provided password is not equal to any of the claim values.
*
* @param claims A map of claim URIs and their respective values to be processed.
* @param userStoreManager The user store manager responsible for user attribute management.
* @param credential The user's authentication credential.
* @throws UserStoreException If the password is found to be equal to a claim value.
*/
private void validatePasswordNotEqualToClaims(Map<String, String> claims,
UserStoreManager userStoreManager, Object credential)
throws UserStoreException {

processClaims(null, claims, null, userStoreManager, credential, false, null);
}

/**
* Processes claims to validate uniqueness and check the password policy.
*
* @param username The username of the user (nullable if not checking duplicates).
* @param claims A map of claim URIs and their values.
* @param profile The user profile (nullable if not checking duplicates).
* @param userStoreManager The user store manager handling claims.
* @param credential The user's password.
* @param duplicateClaims A list to collect duplicate claims (used only if checking duplicates).
* @throws UserStoreException If a policy violation occurs.
*/
private void processClaims(String username, Map<String, String> claims, String profile,
UserStoreManager userStoreManager, Object credential, boolean checkForDuplicates,
List<String> duplicateClaims) throws UserStoreException {

String tenantDomain = getTenantDomain(userStoreManager);
List<String> duplicateClaim = new ArrayList<>();
Claim claimObject = null;

for (Map.Entry<String, String> claim : claims.entrySet()) {
try {
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) {
log.error("Error while getting claim from claimUri: " + claim.getKey() + ".", e);
}
String claimKey = claim.getKey();
String claimValue = claim.getValue();
ClaimConstants.ClaimUniquenessScope uniquenessScope = getClaimUniquenessScope(claimKey, tenantDomain);

if (StringUtils.isNotEmpty(claimValue) && shouldValidateUniqueness(uniquenessScope)) {
Claim claimObject = getClaimObject(userStoreManager, claimKey);
if (claimObject == null) {
continue;
}
// checks whether allowed login identifiers are equal to the password
if (credential != null && (credential.toString()).equals(claim.getValue())) {
errorMessage = "Password can not be equal to the value defined for " +
claimObject.getDisplayTag() + "!";
throw new UserStoreException(errorMessage, new PolicyViolationException(errorMessage));
}
if (isClaimDuplicated(username, claim.getKey(), claim.getValue(), profile, userStoreManager,
uniquenessScope)) {
String displayTag = claimObject.getDisplayTag();
if (StringUtils.isBlank(displayTag)) {
displayTag = claim.getKey();
}
duplicateClaim.add(displayTag);

// Checks whether allowed login identifiers are equal to the password
validatePasswordNotEqualToClaim(credential, claimObject, claimValue);

// Check for duplicate claims if required
if (checkForDuplicates && isClaimDuplicated(username, claimKey, claimValue, profile,
userStoreManager, uniquenessScope)) {
duplicateClaims.add(getClaimDisplayTag(claimObject, claimKey));
}
}
} catch (ClaimMetadataException e) {
log.error("Error while getting claim metadata for claimUri : " + claim.getKey() + ".", e);
log.error("Error while getting claim metadata for claimUri: " + claim.getKey() + ".", e);
}
}
if (StringUtils.isNotBlank(errorMessage)) {
throw new UserStoreException(errorMessage,
new PolicyViolationException(errorMessage));
}

/**
* Retrieves a claim object for a given claim URI.
*
* @param userStoreManager The user store manager handling claims.
* @param claimUri The claim URI to retrieve.
* @return The corresponding claim object, or null if retrieval fails.
*/
private Claim getClaimObject(UserStoreManager userStoreManager, String claimUri) {

try {
return userStoreManager.getClaimManager().getClaim(claimUri);
} catch (org.wso2.carbon.user.api.UserStoreException e) {
log.error("Error while retrieving claim from claimUri: " + claimUri + ".", e);
}
if (duplicateClaim.size() == 0) {
return;
} else if (duplicateClaim.size() == 1) {
errorMessage = "The value defined for " + duplicateClaim.get(0) + " is already in use by different user!";
return null;
}

/**
* Ensures that the password is not equal to any claim value.
*
* @param credential The user's password.
* @param claimObject The claim object being validated.
* @param claimValue The claim value to check against.
* @throws UserStoreException If the password matches the claim value.
*/
private void validatePasswordNotEqualToClaim(Object credential, Claim claimObject, String claimValue)
throws UserStoreException {

if (credential != null && credential.toString().equals(claimValue)) {
String errorMessage = "Password cannot be equal to the value defined for " +
claimObject.getDisplayTag() + "!";
throw new UserStoreException(errorMessage, new PolicyViolationException(errorMessage));
}
}

/**
* Gets the display tag of a claim, falling back to the claim URI if no display tag is available.
*
* @param claimObject The claim object.
* @param claimKey The claim key/URI.
* @return The display name of the claim or the claim key if the display name is not set.
*/
private String getClaimDisplayTag(Claim claimObject, String claimKey) {

return StringUtils.defaultIfBlank(claimObject.getDisplayTag(), claimKey);
}

/**
* Throws an exception if duplicate claims are found.
*
* @param duplicateClaims List of duplicate claim display names.
* @throws UserStoreClientException If duplicate claims are detected.
*/
private void throwDuplicateClaimException(List<String> duplicateClaims) throws UserStoreClientException {

String errorMessage;
if (duplicateClaims.size() == 1) {
errorMessage = "The value defined for " + duplicateClaims.get(0) + " is already in use by a " +
"different user!";
} else {
String claimList = String.join(", ", duplicateClaim);
errorMessage = "The values defined for " + claimList + " are already in use by a different users!";
String claimList = String.join(", ", duplicateClaims);
errorMessage = "The values defined for " + claimList + " are already in use by different users!";
}
throw new UserStoreClientException(errorMessage,
new PolicyViolationException(ERROR_CODE_DUPLICATE_CLAIM_VALUE, errorMessage));
Expand Down Expand Up @@ -339,7 +432,7 @@ public boolean doPreUpdateCredentialByAdmin(String userName, Object newCredentia
for (Claim claim : claims) {
claimMap.put(claim.getClaimUri(), claim.getValue());
}
checkClaimUniqueness(userName, claimMap, null, userStoreManager, newCredential);
validatePasswordNotEqualToClaims(claimMap, userStoreManager, newCredential);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@

package org.wso2.carbon.identity.unique.claim.mgt.listener;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService;
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.model.IdentityEventListenerConfig;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.mgt.policy.PolicyViolationException;
import org.wso2.carbon.identity.unique.claim.mgt.internal.UniqueClaimUserOperationDataHolder;
import org.wso2.carbon.user.api.Claim;
Expand All @@ -41,11 +46,17 @@
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.carbon.user.core.tenant.TenantManager;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
Expand All @@ -56,6 +67,8 @@

public class UniqueClaimUserOperationEventListenerTest {

private static final String EMAIL_CLAIM_URI = "http://wso2.org/claims/emailaddress";

private UniqueClaimUserOperationEventListener uniqueClaimUserOperationEventListener;

@Mock
Expand All @@ -67,8 +80,26 @@ public class UniqueClaimUserOperationEventListenerTest {
@Mock
private Claim claim;

@InjectMocks
private UniqueClaimUserOperationEventListener listener;

@Mock
private ClaimMetadataManagementService claimMetadataManagementService;

@Mock
private RealmService realmService;

@Mock
private TenantManager tenantManager;

@Mock
private IdentityEventListenerConfig mockIdentityEventListenerConfig;

private MockedStatic<IdentityUtil> identityUtilMock;


@BeforeMethod
public void setUp() throws UserStoreException {
public void setUp() throws UserStoreException, ClaimMetadataException {

MockitoAnnotations.initMocks(this);
uniqueClaimUserOperationEventListener = spy(new UniqueClaimUserOperationEventListener());
Expand All @@ -77,6 +108,16 @@ public void setUp() throws UserStoreException {
claim = mock(Claim.class);

when(userStoreManager.getClaimManager()).thenReturn(claimManager);

identityUtilMock = mockStatic(IdentityUtil.class);
claimManager = mock(ClaimManager.class);

when(realmService.getTenantManager()).thenReturn(tenantManager);
when(tenantManager.getDomain(anyInt())).thenReturn("carbon.super");
UniqueClaimUserOperationDataHolder.getInstance().setRealmService(realmService);
UniqueClaimUserOperationDataHolder.getInstance().setRealmService(realmService);
UniqueClaimUserOperationDataHolder.getInstance().setClaimMetadataManagementService(
claimMetadataManagementService);
}

@DataProvider(name = "duplicateClaimDataProvider")
Expand Down Expand Up @@ -160,4 +201,86 @@ public void testDuplicateClaimThrowsException(String userName, Map<String, Strin
throw e;
}
}

@Test
public void testCheckClaimUniquenessWithPasswordPolicyViolation() throws UserStoreException,
NoSuchMethodException, IllegalAccessException, ClaimMetadataException {

mockInitForCheckClaimUniqueness();
String username = "testUser";
Map<String, String> claims = new HashMap<>();
claims.put(EMAIL_CLAIM_URI, "[email protected]");
String profile = "default";
Object credential = "[email protected]";

// Mock the necessary methods.
when(userStoreManager.getTenantId()).thenReturn(1);
when(userStoreManager.getClaimManager()).thenReturn(claimManager);
Claim emailClaimMetaDate = new Claim();
emailClaimMetaDate.setClaimUri(EMAIL_CLAIM_URI);
emailClaimMetaDate.setDisplayTag("Email Address");
when(claimManager.getClaim(anyString())).thenReturn(emailClaimMetaDate);

java.lang.reflect.Method method = UniqueClaimUserOperationEventListener.class.getDeclaredMethod(
"checkClaimUniqueness", String.class, Map.class, String.class, UserStoreManager.class, Object.class);
method.setAccessible(true);
try {
method.invoke(listener, username, claims, profile, userStoreManager, credential);
} catch (InvocationTargetException e) {
assertEquals(e.getTargetException().getClass(), org.wso2.carbon.user.core.UserStoreException.class);
assertTrue(e.getTargetException().getMessage().contains("Password cannot be equal to the value defined " +
"for Email Address!"));
}
}

@Test
public void testValidatePasswordNotEqualToClaims() throws Exception {

Map<String, String> claims = new HashMap<>();
claims.put(EMAIL_CLAIM_URI, "[email protected]");
Object newCredential = "Wso2@test";

// Mock the necessary methods.
when(userStoreManager.getTenantId()).thenReturn(1);
when(userStoreManager.getClaimManager()).thenReturn(claimManager);
Claim emailClaim = new Claim();
emailClaim.setClaimUri(EMAIL_CLAIM_URI);
emailClaim.setDisplayTag("Email Address");
when(claimManager.getClaim(anyString())).thenReturn(emailClaim);

// Use reflection to invoke the private method.
java.lang.reflect.Method method = UniqueClaimUserOperationEventListener.class.getDeclaredMethod(
"validatePasswordNotEqualToClaims", Map.class, UserStoreManager.class, Object.class);
method.setAccessible(true);

try {
// This shouldn't throw any exception. Shouldn't violate the policy.
method.invoke(listener, claims, userStoreManager, newCredential);
} catch (Exception e) {
Assert.fail("Method threw an exception: " + e.getMessage());
}
}

private void mockInitForCheckClaimUniqueness() throws ClaimMetadataException {

List<LocalClaim> localClaims = new ArrayList<>();
LocalClaim emailClaim = new LocalClaim(EMAIL_CLAIM_URI);
emailClaim.setClaimProperty("isUnique", "true");
localClaims.add(emailClaim);
localClaims.add(new LocalClaim("http://wso2.org/claims/username"));
when(claimMetadataManagementService.getLocalClaims(anyString())).thenReturn(localClaims);
identityUtilMock.when(() -> IdentityUtil.readEventListenerProperty(any(), any())).thenReturn(
mockIdentityEventListenerConfig);
Properties properties = new Properties();
properties.put("ScopeWithinUserstore", "true");
when(mockIdentityEventListenerConfig.getProperties()).thenReturn(properties);
}

@AfterMethod
public void tearDown() {

if (identityUtilMock != null) {
identityUtilMock.close();
}
}
}