diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/IdentitySCIMManager.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/IdentitySCIMManager.java index 7b6329a13..ead66d8db 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/IdentitySCIMManager.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/IdentitySCIMManager.java @@ -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 schemaList = new ArrayList<>(); for (AuthenticationSchema authenticationSchema : scimConfigProcessor.getAuthenticationSchemas()) { diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java index a6b7db3c5..2c71137ac 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java @@ -87,8 +87,9 @@ import org.wso2.charon3.core.objects.Group; import org.wso2.charon3.core.objects.Role; import org.wso2.charon3.core.objects.User; -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.charon3.core.protocol.ResponseCodeConstants; import org.wso2.charon3.core.schema.AttributeSchema; import org.wso2.charon3.core.schema.SCIMConstants; @@ -562,15 +563,19 @@ public void deleteUser(String userId) throws NotFoundException, CharonException, @Override @Deprecated public UsersGetResponse listUsersWithGET(Node rootNode, int startIndex, int count, String sortBy, String sortOrder, - String domainName, Map requiredAttributes) + String domainName, Map requiredAttributes) throws CharonException, NotImplementedException, BadRequestException { + //This method will flow down to use offset pagination, so the cursor and the direction will be null. + Cursor cursor = null; + if (sortBy != null || sortOrder != null) { throw new NotImplementedException("Sorting is not supported"); } else if (rootNode != null) { - return filterUsers(rootNode, requiredAttributes, startIndex, count, sortBy, sortOrder, domainName); + return filterUsers(rootNode, requiredAttributes, startIndex, cursor, count, sortBy, sortOrder, + domainName); } else { - return listUsers(requiredAttributes, startIndex, count, sortBy, sortOrder, domainName); + return listUsers(requiredAttributes, startIndex, cursor, count, sortBy, sortOrder, domainName); } } @@ -579,6 +584,9 @@ public UsersGetResponse listUsersWithGET(Node rootNode, Integer startIndex, Inte String sortOrder, String domainName, Map requiredAttributes) throws CharonException, NotImplementedException, BadRequestException { + //This method will flow down to use offset pagination, so the cursor and the direction will be null. + Cursor cursor = null; + // Validate NULL value for startIndex. startIndex = handleStartIndexEqualsNULL(startIndex); if (sortBy != null || sortOrder != null) { @@ -586,9 +594,35 @@ public UsersGetResponse listUsersWithGET(Node rootNode, Integer startIndex, Inte } else if (count != null && count == 0) { return new UsersGetResponse(0, Collections.emptyList()); } else if (rootNode != null) { - return filterUsers(rootNode, requiredAttributes, startIndex, count, sortBy, sortOrder, domainName); + return filterUsers(rootNode, requiredAttributes, startIndex, cursor, count, sortBy, sortOrder, + domainName); + } else { + return listUsers(requiredAttributes, startIndex, cursor, count, sortBy, sortOrder, domainName); + } + } + + @Override + public UsersGetResponse listUsersWithGET(Node rootNode, Cursor cursor, Integer count, String sortBy, + String sortOrder, String domainName, + Map requiredAttributes) + throws CharonException, NotImplementedException, BadRequestException { + + //This method will flow down to use cursor pagination, so the startIndex will be null. + Integer startIndex = null; + // Pagination across multiple domains is not supported when using cursor pagination. + if (domainName == null) { + domainName = UserCoreConstants.PRIMARY_DEFAULT_DOMAIN_NAME; + } + + if (sortBy != null || sortOrder != null) { + throw new NotImplementedException("Sorting is not supported"); + } else if (count != null && count == 0) { + return new UsersGetResponse(0, Collections.emptyList()); + } else if (rootNode != null) { + return filterUsers(rootNode, requiredAttributes, startIndex, cursor, count, sortBy, sortOrder, + domainName); } else { - return listUsers(requiredAttributes, startIndex, count, sortBy, sortOrder, domainName); + return listUsers(requiredAttributes, startIndex, cursor, count, sortBy, sortOrder, domainName); } } @@ -596,45 +630,53 @@ public UsersGetResponse listUsersWithGET(Node rootNode, Integer startIndex, Inte public UsersGetResponse listUsersWithPost(SearchRequest searchRequest, Map requiredAttributes) throws CharonException, NotImplementedException, BadRequestException { - return listUsersWithGET(searchRequest.getFilter(), (Integer) searchRequest.getStartIndex(), - (Integer) searchRequest.getCount(), searchRequest.getSortBy(), searchRequest.getSortOder(), - searchRequest.getDomainName(), requiredAttributes); + if (searchRequest.getCursor() == null) { + return listUsersWithGET(searchRequest.getFilter(), (Integer) searchRequest.getStartIndex(), + (Integer) searchRequest.getCount(), searchRequest.getSortBy(), searchRequest.getSortOder(), + searchRequest.getDomainName(), requiredAttributes); + } else { + return listUsersWithGET(searchRequest.getFilter(), searchRequest.getCursor(), + searchRequest.getCount(), searchRequest.getSortBy(), searchRequest.getSortOder(), + searchRequest.getDomainName(), requiredAttributes); + } + } /** * Method to list users for given conditions. * - * @param requiredAttributes Required attributes for the response - * @param offset Starting index of the count - * @param limit Counting value - * @param sortBy SortBy - * @param sortOrder Sorting order - * @param domainName Name of the user store - * @return User list with detailed attributes - * @throws CharonException Error while listing users + * @param requiredAttributes Required attributes for the response. + * @param offset Starting index of the count. + * @param cursor Cursor value for pagination and the pagination direction. + * @param limit Counting value. + * @param sortBy SortBy. + * @param sortOrder Sorting order. + * @param domainName Name of the user store. + * @return User list with detailed attributes. + * @throws CharonException Error while listing users. * @throws BadRequestException */ - private UsersGetResponse listUsers(Map requiredAttributes, int offset, Integer limit, - String sortBy, String sortOrder, String domainName) throws CharonException, - BadRequestException { + UsersGetResponse listUsers(Map requiredAttributes, Integer offset, Cursor cursor, + Integer limit, String sortBy, String sortOrder, String domainName) + throws CharonException, BadRequestException { List scimUsers = new ArrayList<>(); - // Handle limit equals NULL scenario. limit = handleLimitEqualsNULL(limit); Set coreUsers; long totalUsers = 0; if (StringUtils.isNotEmpty(domainName)) { - if (canPaginate(offset, limit)) { - coreUsers = listUsernames(offset, limit, sortBy, sortOrder, domainName); + if (canPaginate(offset, cursor != null ? cursor.getCursorValue() : null, limit)) { + coreUsers = listUsernames(offset, cursor, limit, sortBy, sortOrder, domainName); totalUsers = getTotalUsers(domainName); - } else { + } else { // Legacy APIs are only used for offset pagination. coreUsers = listUsernamesUsingLegacyAPIs(domainName); if (!SCIMCommonUtils.isConsiderMaxLimitForTotalResultEnabled()) { totalUsers = getTotalUsers(domainName); } } } else { - if (canPaginate(offset, limit)) { + // Listing across all domains is not supported for cursor pagination. + if (canPaginate(offset, null, limit)) { coreUsers = listUsernamesAcrossAllDomains(offset, limit, sortBy, sortOrder); totalUsers += getTotalUsersFromAllUserStores(); } else { @@ -657,12 +699,12 @@ private UsersGetResponse listUsers(Map requiredAttributes, int } else { scimUsers = getUserDetails(coreUsers, requiredAttributes); if (totalUsers != 0) { - totalUsers = Math.toIntExact(totalUsers); // Set total number of results to 0th index. + totalUsers = Math.toIntExact(totalUsers); } else { totalUsers = scimUsers.size(); } } - return getDetailedUsers(scimUsers, (int) totalUsers); + return getDetailedUsers(scimUsers, (int) totalUsers, cursor, domainName); } private long getTotalUsersFromAllUserStores() throws CharonException { @@ -714,31 +756,36 @@ private long getTotalUsers(String domainName) throws CharonException { } /** - * Method to decide whether to paginate based on the offset and the limit in the request. + * Method to decide whether to paginate based on the cursor, the offset and the limit in the request. * - * @param offset Starting index of the count - * @param limit Counting value - * @return true if pagination is possible, false otherwise + * @param offset Starting index of the count. + * @param cursor Cursor value for pagination. + * @param limit Counting value. + * @return true if pagination is possible, false otherwise. */ - private boolean canPaginate(int offset, int limit) { + private boolean canPaginate (Integer offset, String cursor, int limit) { - return (offset != 1 || limit != 0); + if (cursor == null) { + return (offset != 1 || limit != 0); + } + return true; } /** * Method to list paginated usernames from a specific user store using new APIs. * - * @param offset Starting index of the count - * @param limit Counting value - * @param sortBy SortBy - * @param sortOrder Sorting order - * @param domainName Name of the user store - * @return Paginated usernames list - * @throws CharonException Error while listing usernames + * @param offset Starting index of the count. + * @param cursor Cursor value for pagination and pagination direction. + * @param limit Counting value. + * @param sortBy SortBy. + * @param sortOrder Sorting order. + * @param domainName Name of the user store. + * @return Paginated usernames list. + * @throws CharonException Error while listing usernames. * @throws BadRequestException */ - private Set listUsernames(int offset, int limit, String sortBy, - String sortOrder, String domainName) + private Set listUsernames(Integer offset, Cursor cursor, int limit, + String sortBy, String sortOrder, String domainName) throws CharonException, BadRequestException { if (isPaginatedUserStoreAvailable()) { @@ -748,7 +795,7 @@ private Set listUsernames(int offset, int // Operator SW set with USERNAME and empty string to get all users. ExpressionCondition exCond = new ExpressionCondition(ExpressionOperation.SW.toString(), ExpressionAttribute.USERNAME.toString(), ""); - return filterUsernames(exCond, offset, limit, sortBy, sortOrder, domainName); + return filterUsernames(exCond, offset, cursor, limit, sortBy, sortOrder, domainName); } else { if (log.isDebugEnabled()) { log.debug(String.format( @@ -764,7 +811,7 @@ private Set listUsernames(int offset, int * * @param domainName Name of the user store * @return Usernames list - * @throws CharonException Error while listing usernames + * @throws CharonException Error while listing usernames * @throws BadRequestException */ private Set listUsernamesUsingLegacyAPIs(String domainName) @@ -816,7 +863,7 @@ private Set listUsernamesUsingLegacyAPIs( * @param sortBy SortBy * @param sortOrder Sorting order * @return Paginated usernames list - * @throws CharonException Pagination not support + * @throws CharonException Pagination not support * @throws BadRequestException */ private Set listUsernamesAcrossAllDomains(int offset, int limit, @@ -1301,32 +1348,34 @@ private int handleLimitEqualsNULL(Integer limit) { * @param node Filter condition tree. * @param requiredAttributes Required attributes. * @param offset Starting index of the count. + * @param cursor Cursor value for pagination and the pagination direction. * @param limit Number of required results (count). * @param sortBy SortBy. * @param sortOrder Sort order. * @param domainName Domain that the filter should perform. * @return Detailed user list. - * @throws CharonException Error filtering the users. + * @throws CharonException Error filtering the users. * @throws BadRequestException */ - private UsersGetResponse filterUsers(Node node, Map requiredAttributes, int offset, Integer limit, - String sortBy, String sortOrder, String domainName) throws CharonException, - BadRequestException { + private UsersGetResponse filterUsers(Node node, Map requiredAttributes, Integer offset, + Cursor cursor, Integer limit, String sortBy, String sortOrder, + String domainName) + throws CharonException, BadRequestException { // Handle limit equals NULL scenario. limit = handleLimitEqualsNULL(limit); // Handle single attribute search. if (node instanceof ExpressionNode) { - return filterUsersBySingleAttribute((ExpressionNode) node, requiredAttributes, offset, limit, sortBy, - sortOrder, domainName); + return filterUsersBySingleAttribute((ExpressionNode) node, requiredAttributes, offset, cursor, limit, + sortBy, sortOrder, domainName); } else if (node instanceof OperationNode) { if (log.isDebugEnabled()) { log.debug("Listing users by multi attribute filter"); } // Support multi attribute filtering. - return getMultiAttributeFilteredUsers(node, requiredAttributes, offset, limit, sortBy, sortOrder, + return getMultiAttributeFilteredUsers(node, requiredAttributes, offset, cursor, limit, sortBy, sortOrder, domainName); } else { throw new CharonException("Unknown operation. Not either an expression node or an operation node."); @@ -1339,6 +1388,7 @@ private UsersGetResponse filterUsers(Node node, Map requiredAtt * @param node Expression node for single attribute filtering * @param requiredAttributes Required attributes for the response * @param offset Starting index of the count + * @param cursor Cursor value used for pagination and pagination direction. * @param limit Counting value * @param sortBy SortBy * @param sortOrder Sorting order @@ -1348,8 +1398,8 @@ private UsersGetResponse filterUsers(Node node, Map requiredAtt * @throws BadRequestException */ private UsersGetResponse filterUsersBySingleAttribute(ExpressionNode node, Map requiredAttributes, - int offset, int limit, String sortBy, String sortOrder, - String domainName) throws CharonException, BadRequestException { + Integer offset, Cursor cursor, int limit, String sortBy, String sortOrder, + String domainName) throws CharonException, BadRequestException { Set users; List filteredUsers = new ArrayList<>(); @@ -1372,10 +1422,10 @@ private UsersGetResponse filterUsersBySingleAttribute(ExpressionNode node, Map filterUsersByGroup(Node node, * * @param node Expression or Operation node * @param offset Start index value + * @param cursor Cursor value used for pagination and pagination direction. * @param limit Count value * @param sortBy SortBy * @param sortOrder Sort order * @param domainName Domain to perform the search * @return User names of the filtered users - * @throws CharonException Error while filtering + * @throws CharonException Error while filtering * @throws BadRequestException */ - private Set filterUsers(Node node, int offset, int limit, String sortBy, - String sortOrder, String domainName) + private Set filterUsers(Node node, Integer offset, Cursor cursor, + int limit, String sortBy, String sortOrder, String domainName) throws CharonException, BadRequestException { // Filter users when filter by group. - if (SCIMConstants.UserSchemaConstants.GROUP_URI.equals(((ExpressionNode) node).getAttributeValue())) { + if (SCIMConstants.UserSchemaConstants.GROUP_URI.equals(((ExpressionNode) node).getAttributeValue()) + && cursor == null) { return filterUsersByGroup(node, offset, limit, domainName); } // Filter users when the domain is specified in the request. if (StringUtils.isNotEmpty(domainName)) { - return filterUsernames(createConditionForSingleAttributeFilter(domainName, node), offset, limit, + return filterUsernames(createConditionForSingleAttributeFilter(domainName, node), offset, cursor, limit, sortBy, sortOrder, domainName); } else { return filterUsersFromMultipleDomains(node, offset, limit, sortBy, sortOrder, null); @@ -1660,9 +1719,7 @@ private Set filterUsers(Node node, int of * @return User names of the filtered users */ private Set filterUsersFromMultipleDomains(Node node, int offset, int limit, - String sortBy, String sortOrder, - Condition - conditionForListingUsers) + String sortBy, String sortOrder, Condition conditionForListingUsers) throws CharonException, BadRequestException { // Filter users when the domain is not set in the request. Then filter through multiple domains. @@ -1697,7 +1754,8 @@ private Set filterUsersFromMultipleDomain // Filter users for given condition and domain. Set coreUsers; try { - coreUsers = filterUsernames(condition, offset, limit, sortBy, sortOrder, userStoreDomainName); + coreUsers = filterUsernames(condition, offset, null, limit, sortBy, sortOrder, + userStoreDomainName); } catch (CharonException e) { log.error("Error occurred while getting the users list for domain: " + userStoreDomainName, e); continue; @@ -1770,7 +1828,7 @@ private int calculateOffset(Condition condition, int offset, String sortBy, Stri // Checking the number of matches till the original offset. int skippedUserCount; Set skippedUsers = - filterUsernames(condition, initialOffset, offset, sortBy, sortOrder, domainName); + filterUsernames(condition, initialOffset, null, offset, sortBy, sortOrder, domainName); skippedUserCount = skippedUsers.size(); @@ -1781,18 +1839,18 @@ private int calculateOffset(Condition condition, int offset, String sortBy, Stri /** * Method to get users when a filter domain is known. * - * @param condition Condition of the single attribute filter - * @param offset Start index value - * @param limit Count value - * @param sortBy SortBy - * @param sortOrder Sort order - * @param domainName Domain to perform the search - * @return User names of the filtered users - * @throws CharonException Error while filtering + * @param condition Condition of the single attribute filter. + * @param offset Start index value. + * @param cursor Cursor value used for pagination and pagination direction. + * @param limit Count value. + * @param sortBy SortBy. + * @param sortOrder Sort order. + * @param domainName Domain to perform the search. + * @return User names of the filtered users. + * @throws CharonException Error while filtering. */ - private Set filterUsernames(Condition condition, int offset, int limit, - String sortBy, String sortOrder, - String domainName) + private Set filterUsernames(Condition condition, Integer offset, + Cursor cursor, int limit, String sortBy, String sortOrder, String domainName) throws CharonException, BadRequestException { if (log.isDebugEnabled()) { @@ -1800,17 +1858,31 @@ private Set filterUsernames(Condition con offset)); } try { - Set users; - if (removeDuplicateUsersInUsersResponseEnabled) { - users = new TreeSet<>( - Comparator.comparing(org.wso2.carbon.user.core.common.User::getFullQualifiedUsername)); - users.addAll(carbonUM.getUserListWithID(condition, domainName, UserCoreConstants.DEFAULT_PROFILE, limit, - offset, sortBy, sortOrder)); + Set users = null; + if (cursor == null) { + if (removeDuplicateUsersInUsersResponseEnabled) { + users = new TreeSet<>( + Comparator.comparing(org.wso2.carbon.user.core.common.User::getFullQualifiedUsername)); + users.addAll(carbonUM.getUserListWithID(condition, domainName, UserCoreConstants.DEFAULT_PROFILE, + limit, offset, sortBy, sortOrder)); + } else { + List usersList = + carbonUM.getUserListWithID(condition, domainName, UserCoreConstants.DEFAULT_PROFILE, limit, + offset, sortBy, sortOrder); + users = new LinkedHashSet<>(usersList); + } } else { - List usersList = - carbonUM.getUserListWithID(condition, domainName, UserCoreConstants.DEFAULT_PROFILE, limit, - offset, sortBy, sortOrder); - users = new LinkedHashSet<>(usersList); + if (removeDuplicateUsersInUsersResponseEnabled) { + users = new TreeSet<>( + Comparator.comparing(org.wso2.carbon.user.core.common.User::getFullQualifiedUsername)); + users.addAll(carbonUM.getUserListWithID(condition, domainName, UserCoreConstants.DEFAULT_PROFILE, + limit, cursor.getCursorValue(), cursor.getDirection(), sortBy, sortOrder)); + } else { + List usersList = + carbonUM.getUserListWithID(condition, domainName, UserCoreConstants.DEFAULT_PROFILE, limit, + cursor.getCursorValue(), cursor.getDirection(), sortBy, sortOrder); + users = new LinkedHashSet<>(usersList); + } } return users; } catch (UserStoreClientException e) { @@ -1945,18 +2017,32 @@ private Set filterUsersUsingLegacyAPIs(Ex /** * Method to remove duplicate users and get the user details. * - * @param userList List of users retrieved. - * @param totalUsers Total number of results for the query. + * @param userList Filtered user names. + * @param totalUsers Total number of users retrieved. + * @param cursor Cursor object with the cursor value and direction of pagination. + * @param domain User store Domain. * @return Users list with populated attributes * @throws CharonException Error in retrieving user details */ - private UsersGetResponse getDetailedUsers(List userList, int totalUsers) + private UsersGetResponse getDetailedUsers(List userList, int totalUsers, Cursor cursor, String domain) throws CharonException { if (userList == null) { return new UsersGetResponse(0, Collections.emptyList()); + } else if (cursor != null && !userList.isEmpty()) { + String previousCursor = userList.get(0).getUserName(); + String nextCursor = userList.get(userList.size() - 1).getUserName(); + // If the user is not from PRIMARY domain the username will be like: DOMAIN/Username. + if (!UserCoreConstants.PRIMARY_DEFAULT_DOMAIN_NAME.equals(domain)) { + String[] removeDomain = previousCursor.split(CarbonConstants.DOMAIN_SEPARATOR); + previousCursor = removeDomain[1]; + removeDomain = nextCursor.split(CarbonConstants.DOMAIN_SEPARATOR); + nextCursor = removeDomain[1]; + } + return new UsersGetResponse(totalUsers, nextCursor, previousCursor, userList); + } else { + return new UsersGetResponse(totalUsers, userList); } - return new UsersGetResponse(totalUsers, userList); } /** @@ -1965,6 +2051,7 @@ private UsersGetResponse getDetailedUsers(List userList, int totalUsers) * @param node Filter condition tree. * @param requiredAttributes Required attributes. * @param offset Starting index of the count. + * @param cursor Cursor value used for pagination and pagination direction. * @param limit Number of required results (count). * @param sortBy SortBy. * @param sortOrder Sort order. @@ -1973,50 +2060,48 @@ private UsersGetResponse getDetailedUsers(List userList, int totalUsers) * @throws CharonException */ private UsersGetResponse getMultiAttributeFilteredUsers(Node node, Map requiredAttributes, - int offset, int limit, String sortBy, String sortOrder, - String domainName) throws CharonException, BadRequestException { + Integer offset, Cursor cursor, int limit, String sortBy, + String sortOrder, String domainName) + throws CharonException, BadRequestException { List filteredUsers = new ArrayList<>(); Set users; int totalResults = 0; // Handle pagination. if (limit > 0) { - users = getFilteredUsersFromMultiAttributeFiltering(node, offset, limit, sortBy, sortOrder, domainName, - true); + users = getFilteredUsersFromMultiAttributeFiltering(node, offset, cursor, limit, sortBy, sortOrder, + domainName, true); filteredUsers.addAll(getFilteredUserDetails(users, requiredAttributes)); } else { int maxLimit = getMaxLimit(domainName); - users = getMultiAttributeFilteredUsersWithMaxLimit(node, offset, sortBy, sortOrder, domainName, maxLimit); + // Returned users will already be converted to charon User type. + users = getMultiAttributeFilteredUsersWithMaxLimit(node, offset, + cursor, sortBy, sortOrder, domainName, maxLimit); filteredUsers.addAll(getFilteredUserDetails(users, requiredAttributes)); } - // Check that total user count matching the client query needs to be calculated. - if (isJDBCUSerStore(domainName) || isAllConfiguredUserStoresJDBC() || - SCIMCommonUtils.isConsiderTotalRecordsForTotalResultOfLDAPEnabled()) { - int maxLimit = getMaxLimitForTotalResults(domainName); - if (!SCIMCommonUtils.isConsiderMaxLimitForTotalResultEnabled()) { - maxLimit = Math.max(maxLimit, limit); - } - // Get total users based on the filter query. - totalResults += getMultiAttributeFilteredUsersWithMaxLimit(node, 1, sortBy, - sortOrder, domainName, maxLimit).size(); + + int maxLimit = getMaxLimitForTotalResults(domainName); + // Get total users based on the filter query. + if (cursor == null) { + totalResults += getMultiAttributeFilteredUsersWithMaxLimit(node, 1, null, + sortBy, sortOrder, domainName, maxLimit).size(); } else { - totalResults += filteredUsers.size(); - if (totalResults == 0 && filteredUsers.size() > 1) { - totalResults = filteredUsers.size() - 1; - } + Cursor totalResultsCursor = new Cursor(StringUtils.EMPTY, SCIMConstants.NEXT); + totalResults += getMultiAttributeFilteredUsersWithMaxLimit(node, null, + totalResultsCursor, sortBy, sortOrder, domainName, maxLimit).size(); } - return getDetailedUsers(filteredUsers, totalResults); + return getDetailedUsers(filteredUsers, totalResults, cursor, domainName); } - private Set getMultiAttributeFilteredUsersWithMaxLimit(Node node, int offset, - String sortBy, String sortOrder, String domainName, int maxLimit) + private Set getMultiAttributeFilteredUsersWithMaxLimit(Node node, + Integer offset, Cursor cursor, String sortBy, String sortOrder, String domainName, int maxLimit) throws CharonException, BadRequestException { Set users = null; if (StringUtils.isNotEmpty(domainName)) { - users = getFilteredUsersFromMultiAttributeFiltering(node, offset, maxLimit, sortBy, sortOrder, domainName, - false); + users = getFilteredUsersFromMultiAttributeFiltering(node, offset, cursor, maxLimit, sortBy, sortOrder, + domainName, false); return users; } else { AbstractUserStoreManager tempCarbonUM = carbonUM; @@ -2025,8 +2110,8 @@ private Set getMultiAttributeFilteredUser // If carbonUM is not an instance of Abstract User Store Manger we can't get the domain name. if (tempCarbonUM instanceof AbstractUserStoreManager) { domainName = tempCarbonUM.getRealmConfiguration().getUserStoreProperty("DomainName"); - users = getFilteredUsersFromMultiAttributeFiltering(node, offset, maxLimit, sortBy, sortOrder, - domainName, false); + users = getFilteredUsersFromMultiAttributeFiltering(node, offset, cursor, maxLimit, sortBy, + sortOrder, domainName, false); } // If secondary user store manager assigned to carbonUM then global variable carbonUM will contains the // secondary user store manager. @@ -2035,6 +2120,7 @@ private Set getMultiAttributeFilteredUser return users; } } + /** * Get maximum user limit to retrieve. * @@ -2224,6 +2310,7 @@ private Map getMappedAttributes(String extClaimDialectName, Stri * * @param node Filter condition tree. * @param offset Starting index of the count. + * @param cursor Cursor value for pagination pagination direction. * @param limit Number of required results (count). * @param sortBy SortBy. * @param sortOrder Sort order. @@ -2232,15 +2319,11 @@ private Map getMappedAttributes(String extClaimDialectName, Stri * @throws CharonException */ private Set getFilteredUsersFromMultiAttributeFiltering(Node node, - int offset, - int limit, - String sortBy, - String sortOrder, - String domainName, - Boolean paginationRequested) - throws CharonException, BadRequestException { + Integer offset, Cursor cursor, int limit, String sortBy, String sortOrder, String domainName, + Boolean paginationRequested) throws CharonException, BadRequestException { Set coreUsers; + List usersList; try { if (StringUtils.isEmpty(domainName)) { @@ -2256,12 +2339,20 @@ private Set getFilteredUsersFromMultiAttr } else { coreUsers = new LinkedHashSet<>(); } - Condition condition = getCondition(node, attributes); - if (paginationRequested) { - checkForPaginationSupport(condition); - } - coreUsers.addAll(carbonUM.getUserListWithID(condition, domainName, - UserCoreConstants.DEFAULT_PROFILE, limit, offset, sortBy, sortOrder)); + //If the cursor is NULL, then follow the OFFSET pagination flow. + if (cursor == null) { + Condition condition = getCondition(node, attributes); + if (paginationRequested) { + checkForPaginationSupport(condition); + } + usersList = carbonUM.getUserListWithID(getCondition(node, attributes), domainName, + UserCoreConstants.DEFAULT_PROFILE, limit, offset, sortBy, sortOrder); + } else { //Else follow the CURSOR paginated flow. + usersList = carbonUM.getUserListWithID(getCondition(node, attributes), domainName, + UserCoreConstants.DEFAULT_PROFILE, limit, cursor.getCursorValue(), + cursor.getDirection(), sortBy, sortOrder); + } + coreUsers.addAll(usersList); return coreUsers; } catch (UserStoreException e) { throw resolveError(e, "Error in filtering users by multi attributes in domain: " + domainName); @@ -2858,7 +2949,7 @@ private int handleStartIndexEqualsNULL(Integer startIndex) { * @param domainName Domain Name * @param requiredAttributes Required attributes * @return List of groups. - * @throws CharonException If an error occurred. + * @throws CharonException If an error occurred. * @throws BadRequestException If an error occurred. */ private GroupsGetResponse listGroups(int startIndex, Integer count, String sortBy, String sortOrder, String domainName, @@ -4898,7 +4989,7 @@ private List getGroupList(ExpressionNode expressionNode, String domainNa /** * Build ExpressionCondition for scim group filtering with group attributes. * - * @param attributeName Attribute name. + * @param attributeName Attribute name. * @param filterOperation Filter operation. * @param attributeValue Attribute value. * @return ExpressionCondition for the filtering operation. diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/utils/SCIMCommonConstants.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/utils/SCIMCommonConstants.java index fa6f2639d..65d17036f 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/utils/SCIMCommonConstants.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/utils/SCIMCommonConstants.java @@ -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"; diff --git a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java index 5e95881c3..9777c5916 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java @@ -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; @@ -478,6 +479,200 @@ public void testListUsersWithGET(List use assertEquals(result.getUsers().size(), expectedResultCount); } + @Test(dataProvider = "userInfoForCursorFiltering") + public void testCursorFilteringUsersWithGET(String filter, int expectedResultCount, Object cursor, Integer count, + List filteredUsers) + throws Exception { + + Map 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() {{ + 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 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 testUser1Attributes = new HashMap<>(); + testUser1Attributes.put("http://wso2.org/claims/givenname", "testUser"); + testUser1Attributes.put("http://wso2.org/claims/emailaddress", "testUser1@gmail.com"); + 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 testUser2Attributes = new HashMap<>(); + testUser2Attributes.put("http://wso2.org/claims/givenname", "testUser"); + testUser2Attributes.put("http://wso2.org/claims/emailaddress", "testUser2@wso2.com"); + 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 testUser3Attributes = new HashMap<>(); + testUser3Attributes.put("http://wso2.org/claims/givenname", "testUser"); + testUser3Attributes.put("http://wso2.org/claims/emailaddress", "testUser3@gmail.com"); + 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 testUser4Attributes = new HashMap<>(); + testUser4Attributes.put("http://wso2.org/claims/givenname", "testUser"); + testUser4Attributes.put("http://wso2.org/claims/emailaddress", "testUser4@wso2.com"); + 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 testUser5Attributes = new HashMap<>(); + testUser5Attributes.put("http://wso2.org/claims/givenname", "fakeUser"); + testUser5Attributes.put("http://wso2.org/claims/emailaddress", "fakeUser5@gmail.com"); + 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 testUser6Attributes = new HashMap<>(); + testUser6Attributes.put("http://wso2.org/claims/givenname", "fakeUser"); + testUser6Attributes.put("http://wso2.org/claims/emailaddress", "fakeUser6@wso2.com"); + 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 testUser7Attributes = new HashMap<>(); + testUser7Attributes.put("http://wso2.org/claims/givenname", "fakeUser"); + testUser7Attributes.put("http://wso2.org/claims/emailaddress", "fakeUser7@gmail.com"); + fakeUser7.setAttributes(testUser7Attributes); + + return new Object[][]{ + // Forwards pagination initial request. + {"name.givenName eq testUser", 4, new Cursor("", "NEXT"), 5, + new ArrayList() {{ + add(testUser1); + add(testUser2); + add(testUser3); + add(testUser4); + }}}, + + // Forwards pagination without filtering. + {null, 5, new Cursor("fakeUser6", "NEXT"), 5, + new ArrayList() {{ + add(fakeUser7); + add(testUser1); + add(testUser2); + add(testUser3); + add(testUser4); + }}}, + + // Backwards pagination without filter. + {null, 4, new Cursor("testUser2", "PREVIOUS"), 5, + new ArrayList() {{ + 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() {{ + add(testUser3); + add(testUser4); + }}}, + + // Backwards pagination with a filter. + {"name.givenName eq testUser", 2, new Cursor("testUser3", "PREVIOUS"), 5, + new ArrayList() {{ + 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() {{ + 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() {{ + 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() {{ + add(testUser1); + add(testUser3); + }}}, + + // Return empty list when count == 0. + {"", 0, new Cursor("", "NEXT"), 0, + new ArrayList() {{ + }}}, + + // Single attribute group filtering. + {"groups eq Manager", 2, new Cursor("", "NEXT"), 5, + new ArrayList() {{ + add(testUser1); + add(testUser3); + }}}, + }; + } + @DataProvider(name = "listUser") public Object[][] listUser() throws Exception { @@ -856,7 +1051,7 @@ public Object[][] getDataForFilterUsersWithPagination() { add(testUser4); add(testUser5); }}, - false, false, "PRIMARY", 1, 4, 1, 1}, + false, false, "PRIMARY", 1, 4, 1, 2}, {users, "name.givenName sw testUser and name.givenName co New", new ArrayList() {{ @@ -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); } diff --git a/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/resources/UserResource.java b/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/resources/UserResource.java index e71dd18fb..319ac1dc7 100644 --- a/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/resources/UserResource.java +++ b/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/resources/UserResource.java @@ -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; @@ -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. @@ -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) { diff --git a/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/util/SCIMProviderConstants.java b/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/util/SCIMProviderConstants.java index 58b0b5bbb..85d953b3e 100644 --- a/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/util/SCIMProviderConstants.java +++ b/components/org.wso2.carbon.identity.scim2.provider/src/main/java/org/wso2/carbon/identity/scim2/provider/util/SCIMProviderConstants.java @@ -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"; diff --git a/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml b/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml index 10a31a5d7..565063c26 100644 --- a/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml +++ b/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml @@ -31,6 +31,7 @@ false false 100 + true OAuth Bearer Token diff --git a/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml.j2 b/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml.j2 index 9dd1c721c..c10c87f69 100644 --- a/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml.j2 +++ b/features/org.wso2.carbon.identity.scim2.common.feature/resources/charon-config.xml.j2 @@ -31,6 +31,7 @@ true false false + true OAuth Bearer Token