KEYCLOAK-17267 Add index to user attribute name and value to support user sync from ldap
This commit is contained in:
parent
d3e9e21abd
commit
e609949264
8 changed files with 143 additions and 150 deletions
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -56,6 +56,9 @@ public class KeycloakModelParameters {
|
|||
return Stream.empty();
|
||||
}
|
||||
|
||||
public void updateConfig(Config cf) {
|
||||
}
|
||||
|
||||
public Statement classRule(Statement base, Description description) {
|
||||
return base;
|
||||
}
|
||||
|
|
|
@ -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 {};
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {};
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue