Skip to content

Commit

Permalink
Cursor pagination for SCIM resources.
Browse files Browse the repository at this point in the history
Cursor pagination changes phase 1

formatting adjustments before PR

Few changes after the code review

Changes suggested in the code review

Changed strings into constants

Changing the response type from a List to UsersGetResponse type object

Changes to support pagination with POST/.Search. Group filtering changes. Changes to SingleAttribute and MultiAttribute filtering to properly create the UsersGetResponse.

Removing commented code

Made changes for the ServiceProviderConfigEndpoint to show that cursor pagination is supported

Wording updates to the ServiceProviderConfigEndpoint changes

Making changes so that cursor pagination uses PRIMARY domain when a domain is not specified

Removing unused parameters

Adjusting the test cases broken due to changes made and introductig new test cases for the new flows

Adding punctuation to the comments
  • Loading branch information
BojithaPiyathilake committed Jul 15, 2022
1 parent bd07897 commit 44d0b63
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ private void registerCharonConfig() throws CharonException {
Integer.parseInt(scimConfigProcessor.getProperty(SCIMCommonConstants.FILTER_MAX_RESULTS)));
charonConfiguration.setCountValueForPagination
(Integer.parseInt(scimConfigProcessor.getProperty(SCIMCommonConstants.PAGINATION_DEFAULT_COUNT)));
charonConfiguration.setCursorPaginationSupport(
Boolean.parseBoolean(scimConfigProcessor.getProperty(SCIMCommonConstants
.CURSOR_PAGINATION_SUPPORTED)));

ArrayList<Object[]> schemaList = new ArrayList<>();
for (AuthenticationSchema authenticationSchema : scimConfigProcessor.getAuthenticationSchemas()) {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class SCIMCommonConstants {
public static final String CUSTOM_USER_SCHEMA_ENABLED = "custom-user-schema-enabled";
public static final String CUSTOM_USER_SCHEMA_URI = "custom-user-schema-uri";
public static final String ENABLE_REGEX_VALIDATION_FOR_USER_CLAIM_INPUTS = "UserClaimUpdate.EnableUserClaimInputRegexValidation";

public static final String CURSOR_PAGINATION_SUPPORTED = "cursor-pagination-supported";
public static final java.lang.String ASK_PASSWORD_URI = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:askPassword";
public static final java.lang.String VERIFY_EMAIL_URI = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:verifyEmail";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
import org.wso2.carbon.identity.scim2.common.extenstion.SCIMUserStoreErrorResolver;
import org.wso2.carbon.identity.scim2.common.group.SCIMGroupHandler;
import org.wso2.carbon.identity.scim2.common.internal.SCIMCommonComponentHolder;
import org.wso2.charon3.core.objects.plainobjects.UsersGetResponse;
import org.wso2.charon3.core.objects.plainobjects.Cursor;
import org.wso2.charon3.core.objects.plainobjects.GroupsGetResponse;
import org.wso2.charon3.core.objects.plainobjects.UsersGetResponse;
import org.wso2.carbon.identity.scim2.common.test.utils.CommonTestUtils;
import org.wso2.carbon.identity.scim2.common.utils.AttributeMapper;
import org.wso2.carbon.identity.scim2.common.utils.SCIMCommonConstants;
Expand Down Expand Up @@ -478,6 +479,200 @@ public void testListUsersWithGET(List<org.wso2.carbon.user.core.common.User> use
assertEquals(result.getUsers().size(), expectedResultCount);
}

@Test(dataProvider = "userInfoForCursorFiltering")
public void testCursorFilteringUsersWithGET(String filter, int expectedResultCount, Object cursor, Integer count,
List<org.wso2.carbon.user.core.common.User> filteredUsers)
throws Exception {

Map<String, String> scimToLocalClaimMap = new HashMap<>();
scimToLocalClaimMap.put("urn:ietf:params:scim:schemas:core:2.0:User:userName",
"http://wso2.org/claims/username");
scimToLocalClaimMap.put("urn:ietf:params:scim:schemas:core:2.0:id", "http://wso2.org/claims/userid");
scimToLocalClaimMap.put("urn:ietf:params:scim:schemas:core:2.0:User:emails",
"http://wso2.org/claims/emailaddress");
scimToLocalClaimMap.put("urn:ietf:params:scim:schemas:core:2.0:User:name.givenName",
"http://wso2.org/claims/givenname");

mockStatic(SCIMCommonUtils.class);
when(SCIMCommonUtils.getSCIMtoLocalMappings()).thenReturn(scimToLocalClaimMap);
when(SCIMCommonUtils.convertLocalToSCIMDialect(anyMap(), anyMap())).thenReturn(new HashMap<String, String>() {{
put(SCIMConstants.CommonSchemaConstants.ID_URI, "1f70378a-69bb-49cf-aa51-a0493c09110c");
}});

mockedUserStoreManager = PowerMockito.mock(AbstractUserStoreManager.class);

// Cursor filtering.
when(mockedUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), anyInt(),
anyString(), anyString(), anyString(), anyString())).thenReturn(filteredUsers);

whenNew(GroupDAO.class).withAnyArguments().thenReturn(mockedGroupDAO);
when(mockedGroupDAO.listSCIMGroups(anyInt())).thenReturn(anySet());
when(mockedUserStoreManager.getSecondaryUserStoreManager("PRIMARY")).thenReturn(mockedUserStoreManager);
when(mockedUserStoreManager.isSCIMEnabled()).thenReturn(true);
when(mockedUserStoreManager.getSecondaryUserStoreManager("SECONDARY")).thenReturn(secondaryUserStoreManager);
when(secondaryUserStoreManager.isSCIMEnabled()).thenReturn(true);

when(mockedUserStoreManager.getRealmConfiguration()).thenReturn(mockedRealmConfig);
when(mockedRealmConfig.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_MAX_USER_LIST))
.thenReturn("100");

mockStatic(IdentityTenantUtil.class);
when(IdentityTenantUtil.getRealmService()).thenReturn(mockRealmService);
when(mockRealmService.getBootstrapRealmConfiguration()).thenReturn(mockedRealmConfig);
mockStatic(IdentityUtil.class);
when(IdentityUtil.isGroupsVsRolesSeparationImprovementsEnabled()).thenReturn(false);

ClaimMapping[] claimMappings = getTestClaimMappings();
when(mockedClaimManager.getAllClaimMappings(anyString())).thenReturn(claimMappings);

HashMap<String, Boolean> requiredClaimsMap = new HashMap<>();
requiredClaimsMap.put("urn:ietf:params:scim:schemas:core:2.0:User:userName", false);
SCIMUserManager scimUserManager = new SCIMUserManager(mockedUserStoreManager, mockedClaimManager);

Node node = null;
if (StringUtils.isNotBlank(filter)) {
SCIMResourceTypeSchema schema = SCIMResourceSchemaManager.getInstance().getUserResourceSchema();
FilterTreeManager filterTreeManager = new FilterTreeManager(filter, schema);
node = filterTreeManager.buildTree();
}

UsersGetResponse result = scimUserManager.listUsersWithGET(node, (Cursor) cursor, count, null, null, null,
requiredClaimsMap);
assertEquals(result.getUsers().size(), expectedResultCount);
}

@DataProvider(name = "userInfoForCursorFiltering")
public Object[][] userInfoForCursorFiltering() {


org.wso2.carbon.user.core.common.User testUser1 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "testUser1", "testUser1");
Map<String, String> testUser1Attributes = new HashMap<>();
testUser1Attributes.put("http://wso2.org/claims/givenname", "testUser");
testUser1Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
testUser1.setAttributes(testUser1Attributes);

org.wso2.carbon.user.core.common.User testUser2 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "testUser2", "testUser2");
Map<String, String> testUser2Attributes = new HashMap<>();
testUser2Attributes.put("http://wso2.org/claims/givenname", "testUser");
testUser2Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
testUser2.setAttributes(testUser2Attributes);

org.wso2.carbon.user.core.common.User testUser3 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "testUser3", "testUser3");
Map<String, String> testUser3Attributes = new HashMap<>();
testUser3Attributes.put("http://wso2.org/claims/givenname", "testUser");
testUser3Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
testUser3.setAttributes(testUser3Attributes);

org.wso2.carbon.user.core.common.User testUser4 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "testUser4", "testUser4");
Map<String, String> testUser4Attributes = new HashMap<>();
testUser4Attributes.put("http://wso2.org/claims/givenname", "testUser");
testUser4Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
testUser4.setAttributes(testUser4Attributes);

org.wso2.carbon.user.core.common.User fakeUser5 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "fakeUser5", "fakeUser5");
Map<String, String> testUser5Attributes = new HashMap<>();
testUser5Attributes.put("http://wso2.org/claims/givenname", "fakeUser");
testUser5Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
fakeUser5.setAttributes(testUser5Attributes);

org.wso2.carbon.user.core.common.User fakeUser6 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "fakeUser6", "fakeUser6");
Map<String, String> testUser6Attributes = new HashMap<>();
testUser6Attributes.put("http://wso2.org/claims/givenname", "fakeUser");
testUser6Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
fakeUser6.setAttributes(testUser6Attributes);

org.wso2.carbon.user.core.common.User fakeUser7 = new org.wso2.carbon.user.core.common.User(UUID.randomUUID()
.toString(), "fakeUser7", "fakeUser7");
Map<String, String> testUser7Attributes = new HashMap<>();
testUser7Attributes.put("http://wso2.org/claims/givenname", "fakeUser");
testUser7Attributes.put("http://wso2.org/claims/emailaddress", "[email protected]");
fakeUser7.setAttributes(testUser7Attributes);

return new Object[][]{
// Forwards pagination initial request.
{"name.givenName eq testUser", 4, new Cursor("", "NEXT"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(testUser1);
add(testUser2);
add(testUser3);
add(testUser4);
}}},

// Forwards pagination without filtering.
{null, 5, new Cursor("fakeUser6", "NEXT"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(fakeUser7);
add(testUser1);
add(testUser2);
add(testUser3);
add(testUser4);
}}},

// Backwards pagination without filter.
{null, 4, new Cursor("testUser2", "PREVIOUS"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(fakeUser5);
add(fakeUser6);
add(fakeUser7);
add(testUser1);
}}},

// Forwards pagination with a filter.
{"name.givenName eq testUser", 2, new Cursor("testUser2", "NEXT"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(testUser3);
add(testUser4);
}}},

// Backwards pagination with a filter.
{"name.givenName eq testUser", 2, new Cursor("testUser3", "PREVIOUS"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(testUser1);
add(testUser2);
}}},

// Multi-attribute filtering - Forwards pagination - With a count.
{"name.givenName eq testUser and emails co gmail", 2, new Cursor("", "NEXT"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(testUser1);
add(testUser3);
}}},

// Multi-attribute filtering - Backwards pagination.
{"name.givenName eq fakeUser and emails co wso2.com", 1,
new Cursor("fakeUser7", "PREVIOUS"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(fakeUser6);
}}},

// Multi-attribute filtering - Forwards pagination - Without maxLimit calls
// getMultiAttributeFilteredUsersWithMaxLimit.
{"name.givenName eq testUser and emails co gmail", 2, new Cursor("", "NEXT"), null,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(testUser1);
add(testUser3);
}}},

// Return empty list when count == 0.
{"", 0, new Cursor("", "NEXT"), 0,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
}}},

// Single attribute group filtering.
{"groups eq Manager", 2, new Cursor("", "NEXT"), 5,
new ArrayList<org.wso2.carbon.user.core.common.User>() {{
add(testUser1);
add(testUser3);
}}},
};
}

@DataProvider(name = "listUser")
public Object[][] listUser() throws Exception {

Expand Down Expand Up @@ -1444,7 +1639,7 @@ public void testListUsersWithPost() throws Exception {
SCIMUserManager scimUserManager = spy(new SCIMUserManager(mockedUserStoreManager,
mockClaimMetadataManagementService, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME));
doReturn(usersGetResponse).when(scimUserManager)
.listUsersWithGET(any(), any(), any(), anyString(), anyString(), anyString(), anyMap());
.listUsersWithGET(any(), (Integer) any(), any(), anyString(), anyString(), anyString(), anyMap());
UsersGetResponse users = scimUserManager.listUsersWithPost(searchRequest, requiredAttributes);
assertEquals(users, usersGetResponse);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@

import org.wso2.carbon.identity.jaxrs.designator.PATCH;
import org.wso2.carbon.identity.scim2.common.impl.IdentitySCIMManager;
import org.wso2.carbon.identity.scim2.common.utils.SCIMConfigProcessor;
import org.wso2.carbon.identity.scim2.provider.util.SCIMProviderConstants;
import org.wso2.carbon.identity.scim2.provider.util.SupportUtils;
import org.wso2.charon3.core.config.SCIMConfigConstants;
import org.wso2.charon3.core.exceptions.CharonException;
import org.wso2.charon3.core.exceptions.FormatNotSupportedException;
import org.wso2.charon3.core.extensions.UserManager;
import org.wso2.charon3.core.protocol.SCIMResponse;
import org.wso2.charon3.core.protocol.endpoints.UserResourceManager;
import org.wso2.charon3.core.schema.SCIMConstants;
import org.wso2.charon3.core.utils.ResourceManagerUtil;

import java.util.Objects;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
Expand Down Expand Up @@ -162,7 +166,8 @@ public Response getUser(@HeaderParam(SCIMProviderConstants.ACCEPT_HEADER) String
@QueryParam (SCIMProviderConstants.COUNT) Integer count,
@QueryParam (SCIMProviderConstants.SORT_BY) String sortBy,
@QueryParam (SCIMProviderConstants.SORT_ORDER) String sortOrder,
@QueryParam (SCIMProviderConstants.DOMAIN) String domainName) {
@QueryParam (SCIMProviderConstants.DOMAIN) String domainName,
@QueryParam (SCIMProviderConstants.CURSOR) String cursor) {

try {
// defaults to application/scim+json.
Expand All @@ -185,9 +190,24 @@ public Response getUser(@HeaderParam(SCIMProviderConstants.ACCEPT_HEADER) String

SCIMResponse scimResponse;

//Check pagination type.
String paginationType = ResourceManagerUtil.processPagination(startIndex, cursor);

if (SCIMProviderConstants.CURSOR.equals(paginationType)) {
//If the count is null when using a cursor pagination, set count to the value of
//pagination_default_count specified in the server config (charon-config.xml)
if (count == null) {
count = Integer.parseInt(SCIMConfigProcessor.getInstance().
getProperty(SCIMConfigConstants.PAGINATION_DEFAULT_COUNT));
}
scimResponse = userResourceManager.listWithGET(userManager, filter, cursor, count,
sortBy, sortOrder, domainName, attribute, excludedAttributes);
return SupportUtils.buildResponse(scimResponse);
}
scimResponse = userResourceManager.listWithGET(userManager, filter, startIndex, count,
sortBy, sortOrder, domainName, attribute, excludedAttributes);
return SupportUtils.buildResponse(scimResponse);

} catch (CharonException e) {
return handleCharonException(e);
} catch (FormatNotSupportedException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class SCIMProviderConstants {
public static final String ACCEPT_HEADER = "Accept";
public static final String ID = "id";
public static final String DOMAIN = "domain";
public static final String CURSOR = "cursor";
public static final String OFFSET = "offset";

public static final String RESOURCE_STRING = "RESOURCE_STRING";
public static final String HTTP_VERB = "HTTP_VERB";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<Property name="sort-supported">false</Property>
<Property name="etag-supported">false</Property>
<Property name="pagination-default-count">100</Property>
<Property name="cursor-pagination-supported">true</Property>
<authenticationSchemes>
<schema id="1">
<Property name="name">OAuth Bearer Token</Property>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<Property name="changePassword">true</Property>
<Property name="sort-supported">false</Property>
<Property name="etag-supported">false</Property>
<Property name="cursor-pagination-supported">true</Property>
<authenticationSchemes>
<schema id="1">
<Property name="name">OAuth Bearer Token</Property>
Expand Down

0 comments on commit 44d0b63

Please sign in to comment.