Skip to content
Merged
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 @@ -149,6 +149,13 @@ The response uses the SCIM list format:

For filtering and pagination of results, see <<_scim_filtering_resources,Filtering resources>>.

NOTE: The SCIM `/Users` endpoint only returns users that are stored locally in the {project_name} database. Users
that are managed by external user federation providers (such as LDAP or custom providers) and have not been imported
into {project_name} are not included in SCIM search results. Federated users that have been imported (that is,
synchronized into the local database) are included. This is because {project_name} acts as the identity authority
only for locally stored users - external identity sources should expose their own SCIM endpoints for managing
their users.

== Selecting attributes

You can control which attributes are included in the response for both single-resource and list requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.keycloak.scim.resource.schema.attribute.Attribute;
import org.keycloak.scim.resource.spi.AbstractScimResourceTypeProvider;
import org.keycloak.scim.resource.user.User;
import org.keycloak.storage.UserStoragePrivateUtil;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
Expand Down Expand Up @@ -100,7 +101,7 @@ protected User onUpdate(UserModel model, User resource) {
@Override
protected UserModel getModel(String id) {
RealmModel realm = session.getContext().getRealm();
UserModel model = session.users().getUserById(realm, id);
UserModel model = UserStoragePrivateUtil.userLocalStorage(session).getUserById(realm, id);
Comment thread
pedroigor marked this conversation as resolved.
Resolved

if (model == null || model.getServiceAccountClientLink() == null) {
return model;
Expand Down Expand Up @@ -148,7 +149,7 @@ protected Stream<UserModel> getModels(SearchRequest searchRequest) {
.map(entity -> session.users().getUserById(realm, entity.getId()))
.filter(Objects::nonNull));
} else {
return session.users().searchForUserStream(realm, Map.of(UserModel.INCLUDE_SERVICE_ACCOUNT, "false"), searchRequest.getStartIndex() - 1, searchRequest.getCount());
return UserStoragePrivateUtil.userLocalStorage(session).searchForUserStream(realm, Map.of(UserModel.INCLUDE_SERVICE_ACCOUNT, "false"), searchRequest.getStartIndex() - 1, searchRequest.getCount());
}
}

Expand All @@ -171,7 +172,7 @@ public Long count(SearchRequest searchRequest, int resourceSize) {
query.select(cb.countDistinct(root)).where(predicates);
return em.createQuery(query).getSingleResult();
} else {
return (long) session.users().getUsersCount(realm, false);
return (long) UserStoragePrivateUtil.userLocalStorage(session).getUsersCount(realm, false);
Comment thread
sguilhen marked this conversation as resolved.
Resolved
}
}

Expand Down
6 changes: 6 additions & 0 deletions scim/tests/base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.tests</groupId>
<artifactId>keycloak-tests-custom-providers</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.keycloak.tests.scim.tck;

import java.util.List;
import java.util.stream.Collectors;

import org.keycloak.common.Profile.Feature;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.scim.client.ResourceFilter;
import org.keycloak.scim.protocol.response.ListResponse;
import org.keycloak.scim.resource.user.User;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testframework.util.ApiUtil;

import org.junit.jupiter.api.Test;

import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

/**
* Tests that verify SCIM search behavior with non-imported federated users.
*
* @see <a href="https://github.com/keycloak/keycloak/issues/51343">Issue 51343</a>
*/
@KeycloakIntegrationTest(config = FederatedUserTest.FederatedUserServerConfig.class)
public class FederatedUserTest extends AbstractScimTest {

private static final String PROVIDER_ID = "user-password-map-arq";

/**
* Verifies that SCIM filtered and unfiltered searches return consistent results
* when non-imported federated users exist.
*
* Before the fix, unfiltered searches went through UserStorageManager (which
* queries federated providers), while filtered searches used direct JPA queries
* against the local UserEntity table - so non-imported federated users would
* only appear in unfiltered results.
*/
@Test
public void testFilteredAndUnfilteredSearchConsistency() {
// create local users first, before registering the federation provider -
// otherwise the provider (which implements UserRegistrationProvider) intercepts
// user creation and the users end up as federated, not local.
createLocalUser("local-alice");
createLocalUser("local-bob");

// register the federation provider with import disabled and create federated users
String federationComponentId = registerFederationProvider();
try {
String charlieId = createFederatedUser("fed-charlie");
String dianaId = createFederatedUser("fed-diana");

// unfiltered SCIM search - now uses local storage after the fix
ListResponse<User> unfilteredResponse = client.users().getAll();
Comment thread
sguilhen marked this conversation as resolved.
Resolved
assertNotNull(unfilteredResponse);
List<String> unfilteredUserNames = toUserNames(unfilteredResponse);

// filtered SCIM search (broad filter matching all users) - goes through JPA
String broadFilter = ResourceFilter.filter().pr("userName").build();
ListResponse<User> filteredResponse = client.users().getAll(broadFilter);
assertNotNull(filteredResponse);
List<String> filteredUserNames = toUserNames(filteredResponse);

// both local users should appear in both result sets
assertThat(unfilteredUserNames, hasItems("local-alice", "local-bob"));
assertThat(filteredUserNames, hasItems("local-alice", "local-bob"));

// both searches should return the same number of results
assertEquals(unfilteredResponse.getTotalResults(), filteredResponse.getTotalResults(),
"Filtered and unfiltered SCIM searches should return the same number of results. "
+ "Unfiltered returned: " + unfilteredUserNames + ", filtered returned: " + filteredUserNames);

// non-imported federated users should not appear in SCIM results
assertThat(unfilteredUserNames, not(hasItem("fed-charlie")));
assertThat(unfilteredUserNames, not(hasItem("fed-diana")));
assertThat(filteredUserNames, not(hasItem("fed-charlie")));
assertThat(filteredUserNames, not(hasItem("fed-diana")));

// non-imported federated users should not be retrievable by ID either
assertNull(client.users().get(charlieId), "GET by ID should not return non-imported federated user");
assertNull(client.users().get(dianaId), "GET by ID should not return non-imported federated user");
} finally {
realm.admin().components().component(federationComponentId).remove();
}
}

private List<String> toUserNames(ListResponse<User> response) {
return response.getResources().stream()
.map(User::getUserName)
.collect(Collectors.toList());
}

private void createLocalUser(String username) {
User user = new User();
user.setUserName(username);
user.setActive(true);
user = client.users().create(user);
assertNotNull(user);
}

private String registerFederationProvider() {
ComponentRepresentation provider = new ComponentRepresentation();
provider.setName("test-user-federation");
provider.setProviderId(PROVIDER_ID);
provider.setProviderType(UserStorageProvider.class.getName());
provider.setConfig(new MultivaluedHashMap<String, String>());
provider.getConfig().putSingle("priority", Integer.toString(0));
provider.getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(false));
Comment thread
sguilhen marked this conversation as resolved.
Resolved

return ApiUtil.getCreatedId(realm.admin().components().add(provider));
Comment thread
sguilhen marked this conversation as resolved.
Resolved
}

private String createFederatedUser(String username) {
UserRepresentation user = new UserRepresentation();
user.setUsername(username);
user.setEnabled(true);
return ApiUtil.getCreatedId(realm.admin().users().create(user));
}

public static class FederatedUserServerConfig implements KeycloakServerConfig {

@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config
.features(Feature.SCIM_API)
.dependency("org.keycloak.tests", "keycloak-tests-custom-providers");
}
}
}
Loading