KEYCLOAK-16405 Tests for storage logical layer

This commit is contained in:
Hynek Mlnarik 2020-11-20 11:07:07 +01:00 committed by Hynek Mlnařík
parent 4f330f4a57
commit 363df6cab4
18 changed files with 1080 additions and 75 deletions

View file

@ -117,10 +117,6 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core</artifactId>
</dependency>
<dependency>
<groupId>${jdbc.mvn.groupId}</groupId>
<artifactId>${jdbc.mvn.artifactId}</artifactId>

View file

@ -56,6 +56,11 @@ public abstract class UserModelDefaultMethods implements UserModel {
setSingleAttribute(EMAIL, email);
}
@Override
public String toString() {
return getClass().getName() + "@" + getId();
}
/**
* The {@link UserModelDefaultMethods.Streams} class extends the {@link UserModelDefaultMethods} abstract class and
* implements the {@link UserModel.Streams} interface, allowing subclasses to focus on the implementation of the

View file

@ -18,12 +18,12 @@
package org.keycloak.testsuite.federation;
import java.util.Hashtable;
import java.util.Map;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.storage.UserStorageProviderFactory;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -32,7 +32,7 @@ public class BackwardsCompatibilityUserStorageFactory implements UserStorageProv
public static final String PROVIDER_ID = "backwards-compatibility-storage";
protected Map<String, BackwardsCompatibilityUserStorage.MyUser> userPasswords = new Hashtable<>();
private final Map<String, BackwardsCompatibilityUserStorage.MyUser> userPasswords = new ConcurrentHashMap<>();
@Override
public BackwardsCompatibilityUserStorage create(KeycloakSession session, ComponentModel model) {

149
testsuite/model/pom.xml Normal file
View file

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-testsuite-pom</artifactId>
<version>12.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>keycloak-model-test</artifactId>
<name>Tests for logical storage layer</name>
<description>Tests for storage layer functionality targetting logical layer, i.e. models</description>
<packaging>jar</packaging>
<properties>
<keycloak.connectionsJpa.driver>org.h2.Driver</keycloak.connectionsJpa.driver>
<keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database>
<keycloak.connectionsJpa.user>sa</keycloak.connectionsJpa.user>
<keycloak.connectionsJpa.password></keycloak.connectionsJpa.password>
<keycloak.connectionsJpa.url>jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1</keycloak.connectionsJpa.url>
<jdbc.mvn.groupId>com.h2database</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>h2</jdbc.mvn.artifactId>
<jdbc.mvn.version>${h2.version}</jdbc.mvn.version>
<log4j.configuration>file:${project.build.directory}/dependency/log4j.properties</log4j.configuration>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
</dependency>
<dependency>
<groupId>${jdbc.mvn.groupId}</groupId>
<artifactId>${jdbc.mvn.artifactId}</artifactId>
<version>${jdbc.mvn.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-infinispan</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-testsuite-providers</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<!-- keycloak.model.parameters lists parameter classes from
org.keycloak.model.parameters package and determine enabled providers -->
<keycloak.model.parameters>${keycloak.model.parameters}</keycloak.model.parameters>
<keycloak.connectionsJpa.default.driver>${keycloak.connectionsJpa.driver}</keycloak.connectionsJpa.default.driver>
<keycloak.connectionsJpa.default.database>${keycloak.connectionsJpa.database}</keycloak.connectionsJpa.default.database>
<keycloak.connectionsJpa.default.user>${keycloak.connectionsJpa.user}</keycloak.connectionsJpa.default.user>
<keycloak.connectionsJpa.default.password>${keycloak.connectionsJpa.password}</keycloak.connectionsJpa.default.password>
<keycloak.connectionsJpa.default.url>${keycloak.connectionsJpa.url}</keycloak.connectionsJpa.default.url>
<log4j.configuration>file:${project.build.directory}/test-classes/log4j.properties</log4j.configuration> <!-- for the logging to properly work with tests in the 'other' module -->
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>jpa</id>
<properties>
<keycloak.model.parameters>Jpa</keycloak.model.parameters>
</properties>
</profile>
<profile>
<id>jpa+infinispan</id>
<properties>
<keycloak.model.parameters>Infinispan,Jpa</keycloak.model.parameters>
</properties>
</profile>
<profile>
<id>jpa-federation+infinispan</id>
<properties>
<keycloak.model.parameters>Infinispan,JpaFederation,BackwardsCompatibilityUserStorage</keycloak.model.parameters>
</properties>
</profile>
<profile>
<id>jpa-federation</id>
<properties>
<keycloak.model.parameters>JpaFederation,BackwardsCompatibilityUserStorage</keycloak.model.parameters>
</properties>
</profile>
<profile>
<id>map+infinispan</id>
<properties>
<keycloak.model.parameters>Infinispan,Map,ConcurrentHashMapStorage</keycloak.model.parameters>
</properties>
</profile>
</profiles>
</project>

View file

@ -0,0 +1,45 @@
/*
* 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.model;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import java.util.Set;
/**
*
* @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()));
}
}

View file

@ -0,0 +1,226 @@
/*
* 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.model;
import org.keycloak.Config.Scope;
import org.keycloak.authorization.AuthorizationSpi;
import org.keycloak.authorization.DefaultAuthorizationProviderFactory;
import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.cluster.ClusterSpi;
import org.keycloak.component.ComponentModel;
import org.keycloak.events.EventStoreSpi;
import org.keycloak.executors.DefaultExecutorsProviderFactory;
import org.keycloak.executors.ExecutorsSpi;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.ClientSpi;
import org.keycloak.models.GroupSpi;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmSpi;
import org.keycloak.models.RoleSpi;
import org.keycloak.models.UserSpi;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.Spi;
import org.keycloak.services.DefaultKeycloakSession;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.hamcrest.Matchers;
import org.jboss.logging.Logger;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
/**
* Base of testcases that operate on session level. The tests derived from this class
* will have access to a shared {@link KeycloakSessionFactory} in the {@link #FACTORY}
* field that can be used to obtain a session and e.g. start / stop transaction.
* <p>
* This class expects {@code keycloak.model.parameters} system property to contain
* comma-separated class names that implement {@link KeycloakModelParameters} interface
* to provide list of factories and SPIs that are visible to the {@link KeycloakSessionFactory}
* that is offered to the tests.
* <p>
* If no parameters are set via this property, the tests derived from this class are skipped.
* @author hmlnarik
*/
public abstract class KeycloakModelTest {
private static final Logger LOG = Logger.getLogger(KeycloakModelParameters.class);
protected final Logger log = Logger.getLogger(getClass());
@ClassRule
public static final TestRule GUARANTEE_REQUIRED_FACTORY = new TestRule() {
@Override
public Statement apply(Statement base, Description description) {
Class<?> testClass = description.getTestClass();
while (testClass != Object.class) {
for (RequireProvider ann : testClass.getAnnotationsByType(RequireProvider.class)) {
Assume.assumeThat("Provider must exist: " + ann.value(), FACTORY.getProviderFactory(ann.value()), Matchers.notNullValue());
}
testClass = testClass.getSuperclass();
}
return base;
}
};
private static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(AuthorizationSpi.class)
.add(ClientSpi.class)
.add(ClusterSpi.class)
.add(EventStoreSpi.class)
.add(ExecutorsSpi.class)
.add(GroupSpi.class)
.add(RealmSpi.class)
.add(RoleSpi.class)
.add(StoreFactorySpi.class)
.add(UserSpi.class)
.build();
private static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(DefaultAuthorizationProviderFactory.class)
.add(DefaultExecutorsProviderFactory.class)
.build();
protected static final List<KeycloakModelParameters> MODEL_PARAMETERS;
protected static final DefaultKeycloakSessionFactory FACTORY;
static {
KeycloakModelParameters basicParameters = new KeycloakModelParameters(ALLOWED_SPIS, ALLOWED_FACTORIES);
MODEL_PARAMETERS = Stream.concat(
Stream.of(basicParameters),
Stream.of(System.getProperty("keycloak.model.parameters", "").split("\\s*,\\s*"))
.filter(s -> s != null && ! s.trim().isEmpty())
.map(cn -> { try { return Class.forName(cn.indexOf('.') >= 0 ? cn : ("org.keycloak.model.parameters." + cn)); } catch (Exception e) { LOG.error("Cannot find " + cn); return null; }})
.filter(Objects::nonNull)
.map(c -> { try { return c.newInstance(); } catch (Exception e) { LOG.error("Cannot instantiate " + c); return null; }} )
.filter(KeycloakModelParameters.class::isInstance)
.map(KeycloakModelParameters.class::cast)
)
.collect(Collectors.toList());
FACTORY = new DefaultKeycloakSessionFactory() {
@Override
protected boolean isEnabled(ProviderFactory factory, Scope scope) {
return super.isEnabled(factory, scope) && isFactoryAllowed(factory);
}
@Override
protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> loadFactories(ProviderManager pm) {
spis.removeIf(s -> ! isSpiAllowed(s));
return super.loadFactories(pm);
}
private boolean isSpiAllowed(Spi s) {
return MODEL_PARAMETERS.stream().anyMatch(p -> p.isSpiAllowed(s));
}
private boolean isFactoryAllowed(ProviderFactory factory) {
return MODEL_PARAMETERS.stream().anyMatch(p -> p.isFactoryAllowed(factory));
}
};
FACTORY.init();
}
@BeforeClass
public static void checkValidParameters() {
Assume.assumeTrue("keycloak.model.parameters property must be set", MODEL_PARAMETERS.size() > 1); // Additional parameters have to be set
}
protected void createEnvironment(KeycloakSession s) {
}
protected void cleanEnvironment(KeycloakSession s) {
}
@Before
public void createEnvironment() {
KeycloakModelUtils.runJobInTransaction(FACTORY, this::createEnvironment);
}
@After
public void cleanEnvironment() {
KeycloakModelUtils.runJobInTransaction(FACTORY, this::cleanEnvironment);
}
protected String registerUserFederationIfAvailable(RealmModel realm) {
final List<ProviderFactory> userFedProviders = FACTORY.getProviderFactories(UserStorageProvider.class);
if (! userFedProviders.isEmpty() && realm != null) {
assertThat("Cannot handle more than 1 user federation provider", userFedProviders, hasSize(1));
UserStorageProviderModel federatedStorage = new UserStorageProviderModel();
federatedStorage.setName(userFedProviders.get(0).getId());
federatedStorage.setProviderId(userFedProviders.get(0).getId());
federatedStorage.setProviderType(UserStorageProvider.class.getName());
federatedStorage.setParentId(realm.getId());
ComponentModel res = realm.addComponentModel(federatedStorage);
log.infof("Added %s user federation provider: %s", federatedStorage.getName(), res.getId());
return res.getId();
}
return null;
}
protected <T> void inRolledBackTransaction(T parameter, BiConsumer<KeycloakSession, T> what) {
KeycloakSession session = new DefaultKeycloakSession(FACTORY);
session.getTransactionManager().begin();
what.accept(session, parameter);
session.getTransactionManager().rollback();
}
protected <T> void inComittedTransaction(T parameter, BiConsumer<KeycloakSession, T> what) {
inComittedTransaction(parameter, what, (a,b) -> {}, (a,b) -> {});
}
protected <T> void inComittedTransaction(T parameter, BiConsumer<KeycloakSession, T> what, BiConsumer<KeycloakSession, T> onCommit, BiConsumer<KeycloakSession, T> onRollback) {
KeycloakModelUtils.runJobInTransaction(FACTORY, session -> {
session.getTransactionManager().enlistAfterCompletion(new AbstractKeycloakTransaction() {
@Override
protected void commitImpl() {
if (onCommit != null) { onCommit.accept(session, parameter); }
}
@Override
protected void rollbackImpl() {
if (onRollback != null) { onRollback.accept(session, parameter); }
}
});
what.accept(session, parameter);
});
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.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)
@Repeatable(RequireProviders.class)
public @interface RequireProvider {
Class<? extends Provider> value() default Provider.class;
}

View file

@ -0,0 +1,32 @@
/*
* 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.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)
public @interface RequireProviders {
RequireProvider[] value();
}

View file

@ -0,0 +1,235 @@
/*
* 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.model;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.hamcrest.Matchers;
import org.junit.Test;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeThat;
/**
*
* @author hmlnarik
*/
@RequireProvider(UserProvider.class)
@RequireProvider(RealmProvider.class)
public class UserModelTest extends KeycloakModelTest {
protected static final int NUM_GROUPS = 100;
private String realmId;
private final List<String> groupIds = new ArrayList<>(NUM_GROUPS);
private String userFederationId;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("realm");
this.realmId = realm.getId();
this.userFederationId = registerUserFederationIfAvailable(realm);
IntStream.range(0, NUM_GROUPS).forEach(i -> {
groupIds.add(s.groups().createGroup(realm, "group-" + i).getId());
});
}
@Override
public void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
private void addRemoveUser(KeycloakSession session, int i) {
RealmModel realm = session.realms().getRealmByName("realm");
UserModel user = session.users().addUser(realm, "user-" + i);
IntStream.range(0, NUM_GROUPS / 20).forEach(gIndex -> {
user.joinGroup(session.groups().getGroupById(realm, groupIds.get((i + gIndex) % NUM_GROUPS)));
});
final UserModel obtainedUser = session.users().getUserById(user.getId(), realm);
assertThat(obtainedUser, Matchers.notNullValue());
assertThat(obtainedUser.getUsername(), is("user-" + i));
Set<String> userGroupIds = obtainedUser.getGroupsStream().map(GroupModel::getName).collect(Collectors.toSet());
assertThat(userGroupIds, hasSize(NUM_GROUPS / 20));
assertThat(userGroupIds, hasItem("group-" + i));
assertThat(userGroupIds, hasItem("group-" + (i - 1 + (NUM_GROUPS / 20)) % NUM_GROUPS));
assertTrue(session.users().removeUser(realm, user));
assertFalse(session.users().removeUser(realm, user));
}
@Test
public void testAddRemoveUser() {
inRolledBackTransaction(1, this::addRemoveUser);
}
@Test
public void testAddRemoveUserConcurrent() {
IntStream.range(0,100).parallel().forEach(i -> inComittedTransaction(i, this::addRemoveUser));
}
@Test
public void testAddRemoveUsersInTheSameGroupConcurrent() {
final ConcurrentSkipListSet<String> userIds = new ConcurrentSkipListSet<>();
String groupId = groupIds.get(0);
// Create users and let them join first group
IntStream.range(0, 100).parallel().forEach(index -> inComittedTransaction(index, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final UserModel user = session.users().addUser(realm, "user-" + i);
user.joinGroup(session.groups().getGroupById(realm, groupId));
userIds.add(user.getId());
}));
inComittedTransaction(1, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final GroupModel group = session.groups().getGroupById(realm, groupId);
assertThat(session.users().getGroupMembersStream(realm, group).count(), is(100L));
});
// Some of the transactions may fail due to conflicts as there are many parallel request, so repeat until all users are removed
Set<String> remainingUserIds = new HashSet<>();
do {
userIds.stream().parallel().forEach(index -> inComittedTransaction(index, (session, userId) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final UserModel user = session.users().getUserById(userId, realm);
log.debugf("Remove user %s: %s", userId, session.users().removeUser(realm, user));
}, null, (session, userId) -> remainingUserIds.add(userId) ));
userIds.clear();
userIds.addAll(remainingUserIds);
remainingUserIds.clear();
} while (! userIds.isEmpty());
inComittedTransaction(1, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final GroupModel group = session.groups().getGroupById(realm, groupId);
assertThat(session.users().getGroupMembersStream(realm, group).collect(Collectors.toList()), Matchers.empty());
});
}
@Test
public void testAddDirtyRemoveFederationUsersInTheSameGroupConcurrent() {
assumeThat("Test for federated providers only", userFederationId, Matchers.notNullValue());
final ConcurrentSkipListSet<String> userIds = new ConcurrentSkipListSet<>();
String groupId = groupIds.get(0);
// Create users and let them join first group
IntStream.range(0, 100).parallel().forEach(index -> inComittedTransaction(index, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final UserModel user = session.users().addUser(realm, "user-" + i);
user.joinGroup(session.groups().getGroupById(realm, groupId));
userIds.add(user.getId());
}));
// Remove users _from the federation_, simulates eg. user being removed from LDAP without Keycloak knowing
inComittedTransaction(1, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
UserStorageProvider instance = (UserStorageProvider)session.getAttribute(userFederationId);
if (instance == null) {
ComponentModel model = realm.getComponent(userFederationId);
UserStorageProviderModel storageModel = new UserStorageProviderModel(model);
UserStorageProviderFactory factory = (UserStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, model.getProviderId());
instance = factory.create(session, model);
if (instance == null) {
throw new RuntimeException("UserStorageProvideFactory (of type " + factory.getClass().getName() + ") produced a null instance");
}
session.enlistForClose(instance);
session.setAttribute(userFederationId, instance);
}
final UserStorageProvider lambdaInstance = instance;
log.debugf("Removing selected users from backend");
IntStream.range(FIRST_DELETED_USER_INDEX, LAST_DELETED_USER_INDEX).forEach(j -> {
final UserModel user = ((UserLookupProvider) lambdaInstance).getUserByUsername("user-" + j, realm);
((UserRegistrationProvider) lambdaInstance).removeUser(realm, user);
});
});
inComittedTransaction(1, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final GroupModel group = session.groups().getGroupById(realm, groupId);
assertThat(session.users().getGroupMembersStream(realm, group).count(), is(100L - DELETED_USER_COUNT));
});
// Now delete the users, and count those that were not found to be deleted. This should be equal to the number
// of users removed directly in the user federation.
// Some of the transactions may fail due to conflicts as there are many parallel request, so repeat until all users are removed
AtomicInteger notFoundUsers = new AtomicInteger();
Set<String> remainingUserIds = new HashSet<>();
do {
userIds.stream().parallel().forEach(index -> inComittedTransaction(index, (session, userId) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final UserModel user = session.users().getUserById(userId, realm);
if (user != null) {
log.debugf("Deleting user: %s", userId);
session.users().removeUser(realm, user);
} else {
log.debugf("Failed deleting user: %s", userId);
notFoundUsers.incrementAndGet();
}
}, null, (session, userId) -> {
log.debugf("Could not delete user %s", userId);
remainingUserIds.add(userId);
}));
userIds.clear();
userIds.addAll(remainingUserIds);
remainingUserIds.clear();
} while (! userIds.isEmpty());
assertThat(notFoundUsers.get(), is(DELETED_USER_COUNT));
inComittedTransaction(1, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
final GroupModel group = session.groups().getGroupById(realm, groupId);
assertThat(session.users().getGroupMembersStream(realm, group).collect(Collectors.toList()), Matchers.empty());
});
}
private static final int FIRST_DELETED_USER_INDEX = 10;
private static final int LAST_DELETED_USER_INDEX = 90;
private static final int DELETED_USER_COUNT = LAST_DELETED_USER_INDEX - FIRST_DELETED_USER_INDEX;
}

View file

@ -14,36 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.events.jpa;
package org.keycloak.model.events;
import org.keycloak.Config.Scope;
import org.keycloak.common.ClientConnection;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.JpaConnectionSpi;
import org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.updater.JpaUpdaterSpi;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventStoreProviderFactory;
import org.keycloak.events.EventStoreSpi;
import org.keycloak.events.EventType;
import org.keycloak.model.KeycloakModelTest;
import org.keycloak.model.RequireProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmSpi;
import org.keycloak.models.dblock.DBLockSpi;
import org.keycloak.models.jpa.JpaRealmProviderFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.Spi;
import org.keycloak.services.DefaultKeycloakSession;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import com.google.common.collect.ImmutableSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Before;
@ -55,43 +35,17 @@ import static org.junit.Assert.assertThat;
*
* @author hmlnarik
*/
public class JpaAdminEventQueryTest {
@RequireProvider(EventStoreProvider.class)
public class AdminEventQueryTest extends KeycloakModelTest {
private static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(DBLockSpi.class)
.add(EventStoreSpi.class)
.add(JpaConnectionSpi.class)
.add(JpaUpdaterSpi.class)
.add(LiquibaseConnectionSpi.class)
.add(RealmSpi.class)
.build();
private static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(DefaultJpaConnectionProviderFactory.class)
.add(EventStoreProviderFactory.class)
.add(JpaUpdaterProviderFactory.class)
.add(JpaRealmProviderFactory.class)
.add(LiquibaseConnectionProviderFactory.class)
.add(LiquibaseDBLockProviderFactory.class)
.build();
private static final DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory() {
@Override
protected boolean isEnabled(ProviderFactory factory, Scope scope) {
return super.isEnabled(factory, scope) && ALLOWED_FACTORIES.stream().filter(c -> c.isAssignableFrom(factory.getClass())).findAny().isPresent();
}
@Override
protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> loadFactories(ProviderManager pm) {
spis.removeIf(s -> ! ALLOWED_SPIS.contains(s.getClass()));
return super.loadFactories(pm);
}
};
static { factory.init(); }
private final KeycloakSession session = new DefaultKeycloakSession(factory);
private final KeycloakSession session = FACTORY.create();
private final EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
@Test
public void testClear() {
eventStore.clear();
}
@Before
public void startTransaction() {
session.getTransactionManager().begin();
@ -102,11 +56,6 @@ public class JpaAdminEventQueryTest {
session.getTransactionManager().rollback();
}
@Test
public void testClear() {
eventStore.clear();
}
@Test
public void testQuery() {
RealmModel realm = session.realms().createRealm("realm");

View file

@ -0,0 +1,45 @@
/*
* 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.model.parameters;
import org.keycloak.model.KeycloakModelParameters;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.storage.UserStorageProviderSpi;
import org.keycloak.storage.federated.UserFederatedStorageProviderSpi;
import org.keycloak.storage.jpa.JpaUserFederatedStorageProviderFactory;
import org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/**
*
* @author hmlnarik
*/
public class BackwardsCompatibilityUserStorage extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(BackwardsCompatibilityUserStorageFactory.class)
.build();
public BackwardsCompatibilityUserStorage() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.model.parameters;
import org.keycloak.model.KeycloakModelParameters;
import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.group.MapGroupProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory;
import org.keycloak.models.map.storage.ConcurrentHashMapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/**
*
* @author hmlnarik
*/
public class ConcurrentHashMapStorage extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(ConcurrentHashMapStorageProvider.class)
.build();
static {
System.setProperty("keycloak.mapStorage.concurrenthashmap.dir", System.getProperty("keycloak.mapStorage.concurrenthashmap.dir", "${project.build.directory:target}"));
}
public ConcurrentHashMapStorage() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.model.parameters;
import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
import org.keycloak.model.KeycloakModelParameters;
import org.keycloak.models.cache.CacheRealmProviderSpi;
import org.keycloak.models.cache.CacheUserProviderSpi;
import org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory;
import org.keycloak.models.cache.infinispan.InfinispanUserCacheProviderFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/**
*
* @author hmlnarik
*/
public class Infinispan extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(CacheRealmProviderSpi.class)
.add(CacheUserProviderSpi.class)
.add(InfinispanConnectionSpi.class)
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(InfinispanCacheRealmProviderFactory.class)
.add(InfinispanClusterProviderFactory.class)
.add(InfinispanConnectionProviderFactory.class)
.add(InfinispanUserCacheProviderFactory.class)
.build();
static {
System.setProperty("keycloak.connectionsInfinispan.default.embedded", System.getProperty("keycloak.connectionsInfinispan.default.embedded", "true"));
}
public Infinispan() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
}

View file

@ -0,0 +1,74 @@
/*
* 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.model.parameters;
import org.keycloak.authorization.jpa.store.JPAAuthorizationStoreFactory;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.JpaConnectionSpi;
import org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.updater.JpaUpdaterSpi;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
import org.keycloak.model.KeycloakModelParameters;
import org.keycloak.models.dblock.DBLockSpi;
import org.keycloak.models.jpa.JpaClientProviderFactory;
import org.keycloak.models.jpa.JpaGroupProviderFactory;
import org.keycloak.models.jpa.JpaRealmProviderFactory;
import org.keycloak.models.jpa.JpaRoleProviderFactory;
import org.keycloak.models.jpa.JpaUserProviderFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/**
*
* @author hmlnarik
*/
public class Jpa extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
// jpa-specific
.add(DBLockSpi.class)
.add(JpaConnectionSpi.class)
.add(JpaUpdaterSpi.class)
.add(LiquibaseConnectionSpi.class)
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
// jpa-specific
.add(DefaultJpaConnectionProviderFactory.class)
.add(JPAAuthorizationStoreFactory.class)
.add(JpaClientProviderFactory.class)
.add(JpaEventStoreProviderFactory.class)
.add(JpaGroupProviderFactory.class)
.add(JpaRealmProviderFactory.class)
.add(JpaRoleProviderFactory.class)
.add(JpaUpdaterProviderFactory.class)
.add(JpaUserProviderFactory.class)
.add(LiquibaseConnectionProviderFactory.class)
.add(LiquibaseDBLockProviderFactory.class)
.build();
public Jpa() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.model.parameters;
import org.keycloak.model.KeycloakModelParameters;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import org.keycloak.storage.UserStorageProviderSpi;
import org.keycloak.storage.federated.UserFederatedStorageProviderSpi;
import org.keycloak.storage.jpa.JpaUserFederatedStorageProviderFactory;
import org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/**
*
* @author hmlnarik
*/
public class JpaFederation extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.addAll(Jpa.ALLOWED_SPIS)
.add(UserStorageProviderSpi.class)
.add(UserFederatedStorageProviderSpi.class)
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.addAll(Jpa.ALLOWED_FACTORIES)
.add(JpaUserFederatedStorageProviderFactory.class)
.build();
public JpaFederation() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.model.parameters;
import org.keycloak.model.KeycloakModelParameters;
import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.group.MapGroupProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/**
*
* @author hmlnarik
*/
public class Map extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(MapStorageSpi.class)
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(MapClientProviderFactory.class)
.add(MapGroupProviderFactory.class)
.add(MapRoleProviderFactory.class)
.add(MapStorageProvider.class)
.build();
public Map() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
}

View file

@ -19,18 +19,16 @@ log4j.rootLogger=info, keycloak
log4j.appender.keycloak=org.apache.log4j.ConsoleAppender
log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout
keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n
log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern}
# Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug )
log4j.logger.org.keycloak=${keycloak.logging.level:debug}
keycloak.testsuite.logging.level=debug
log4j.logger.org.keycloak.testsuite=${keycloak.testsuite.logging.level}
log4j.logger.org.keycloak.models=debug
# log4j.logger.org.hibernate=debug
# Enable to view loaded SPI and Providers
log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
log4j.logger.org.keycloak.provider.ProviderManager=debug
# log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
# log4j.logger.org.keycloak.provider.ProviderManager=debug
# log4j.logger.org.keycloak.provider.FileSystemProviderLoaderFactory=debug
# Liquibase updates logged with "info" by default. Logging level can be changed by system property "keycloak.liquibase.logging.level"

View file

@ -45,6 +45,7 @@
<modules>
<module>db-allocator-plugin</module>
<module>integration-arquillian</module>
<module>model</module>
<module>utils</module>
</modules>