KEYCLOAK-17267 Add index to user attribute name and value to support user sync from ldap

This commit is contained in:
mhajas 2021-05-14 14:52:43 +02:00 committed by Hynek Mlnařík
parent d3e9e21abd
commit e609949264
8 changed files with 143 additions and 150 deletions

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2021 Red Hat, Inc. and/or its affiliates
~ * and other contributors as indicated by the @author tags.
~ *
~ * Licensed under the Apache License, Version 2.0 (the "License");
~ * you may not use this file except in compliance with the License.
~ * You may obtain a copy of the License at
~ *
~ * http://www.apache.org/licenses/LICENSE-2.0
~ *
~ * Unless required by applicable law or agreed to in writing, software
~ * distributed under the License is distributed on an "AS IS" BASIS,
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ * See the License for the specific language governing permissions and
~ * limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="KEYCLOAK-17267-add-index-to-user-attributes">
<createIndex indexName="IDX_USER_ATTRIBUTE_NAME" tableName="USER_ATTRIBUTE">
<column name="NAME" type="VARCHAR(255)"/>
<column name="VALUE" type="VARCHAR(255)"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View file

@ -69,5 +69,6 @@
<include file="META-INF/jpa-changelog-11.0.0.xml"/>
<include file="META-INF/jpa-changelog-12.0.0.xml"/>
<include file="META-INF/jpa-changelog-13.0.0.xml"/>
<include file="META-INF/jpa-changelog-14.0.0.xml"/>
</databaseChangeLog>

View file

@ -56,6 +56,9 @@ public class KeycloakModelParameters {
return Stream.empty();
}
public void updateConfig(Config cf) {
}
public Statement classRule(Statement base, Description description) {
return base;
}

View file

@ -35,4 +35,10 @@ import java.lang.annotation.Target;
public @interface RequireProvider {
Class<? extends Provider> value() default Provider.class;
/**
* Specifies provider IDs of mandatory provider. There must be at least one provider available
* from those in {@code only} array to fulfil this requirement.
*/
String[] only() default {};
}

View file

@ -1,74 +0,0 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.model;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.testsuite.model.Config.SpiConfig;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Set;
import java.util.stream.Stream;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
*
* @author hmlnarik
*/
public class KeycloakModelParameters {
private final Set<Class<? extends Spi>> allowedSpis;
private final Set<Class<? extends ProviderFactory>> allowedFactories;
public KeycloakModelParameters(Set<Class<? extends Spi>> allowedSpis, Set<Class<? extends ProviderFactory>> allowedFactories) {
this.allowedSpis = allowedSpis;
this.allowedFactories = allowedFactories;
}
boolean isSpiAllowed(Spi s) {
return allowedSpis.contains(s.getClass());
}
boolean isFactoryAllowed(ProviderFactory factory) {
return allowedFactories.stream().anyMatch((c) -> c.isAssignableFrom(factory.getClass()));
}
/**
* Returns stream of parameters of the given type, or an empty stream if no parameters of the given type are supplied
* by this clazz.
* @param <T>
* @param clazz
* @return
*/
public <T> Stream<T> getParameters(Class<T> clazz) {
return Stream.empty();
}
public void updateConfig(Config cf) {
}
public Statement classRule(Statement base, Description description) {
return base;
}
public Statement instanceRule(Statement base, Description description) {
return base;
}
}

View file

@ -1,44 +0,0 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.model;
import org.keycloak.provider.Provider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Identifies a requirement for a given provider to be present in the session factory.
* If the provider is not available, the test is skipped.
*
* @author hmlnarik
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Repeatable(RequireProviders.class)
public @interface RequireProvider {
Class<? extends Provider> value() default Provider.class;
/**
* Specifies provider IDs of mandatory provider. There must be at least one provider available
* from those in {@code only} array to fulfil this requirement.
*/
String[] only() default {};
}

View file

@ -1,32 +0,0 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.model;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* @author hmlnarik
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequireProviders {
RequireProvider[] value();
}

View file

@ -0,0 +1,106 @@
package org.keycloak.testsuite.model;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserProvider;
import org.keycloak.services.managers.UserStorageSyncManager;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.storage.user.SynchronizationResult;
import org.keycloak.testsuite.util.LDAPTestUtils;
import java.util.stream.IntStream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assume.assumeThat;
@RequireProvider(UserProvider.class)
@RequireProvider(ClusterProvider.class)
@RequireProvider(RealmProvider.class)
@RequireProvider(value = UserStorageProvider.class, only = LDAPStorageProviderFactory.PROVIDER_NAME)
public class UserSyncTest extends KeycloakModelTest {
private static final int NUMBER_OF_USERS = 5000;
private String realmId;
private String userFederationId;
@Override
public void createEnvironment(KeycloakSession s) {
inComittedTransaction(session -> {
RealmModel realm = session.realms().createRealm("realm");
realm.setDefaultRole(session.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
});
getParameters(UserStorageProviderModel.class).forEach(fs -> inComittedTransaction(session -> {
if (userFederationId != null || !fs.isImportEnabled()) return;
RealmModel realm = session.realms().getRealm(realmId);
fs.setParentId(realmId);
ComponentModel res = realm.addComponentModel(fs);
// Check if the provider implements ImportSynchronization interface
UserStorageProviderFactory userStorageProviderFactory = (UserStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, res.getProviderId());
if (!ImportSynchronization.class.isAssignableFrom(userStorageProviderFactory.getClass())) {
return;
}
userFederationId = res.getId();
log.infof("Added %s user federation provider: %s", fs.getName(), res.getId());
}));
assumeThat("Cannot run UserSyncTest because there is no user federation provider that supports sync", userFederationId, notNullValue());
}
@Override
public void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
@Override
protected boolean isUseSameKeycloakSessionFactoryForAllThreads() {
return true;
}
@Test
public void testManyUsersImport() {
IntStream.range(0, NUMBER_OF_USERS).parallel().forEach(index -> inComittedTransaction(index, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
ComponentModel ldapModel = LDAPTestUtils.getLdapProviderModel(realm);
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
LDAPTestUtils.addLDAPUser(ldapFedProvider, realm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", null, "12" + i);
return null;
}));
assertThat(withRealm(realmId, (session, realm) -> session.userLocalStorage().getUsersCount(realm)), is(0));
long start = System.currentTimeMillis();
SynchronizationResult res = withRealm(realmId, (session, realm) -> {
UserStorageProviderModel providerModel = new UserStorageProviderModel(realm.getComponent(userFederationId));
return new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), providerModel);
});
long end = System.currentTimeMillis();
long timeNeeded = end - start;
// The sync shouldn't take more than 18 second per user
assertThat(String.format("User sync took %f seconds per user, but it should take less than 18 seconds",
(float)(timeNeeded) / NUMBER_OF_USERS), timeNeeded, Matchers.lessThan((long) (18 * NUMBER_OF_USERS)));
assertThat(res.getAdded(), is(NUMBER_OF_USERS));
assertThat(withRealm(realmId, (session, realm) -> session.userLocalStorage().getUsersCount(realm)), is(NUMBER_OF_USERS));
}
}