diff --git a/examples/as7-eap-demo/server/pom.xml b/examples/as7-eap-demo/server/pom.xml index 7027dc52cb..f7e619e543 100755 --- a/examples/as7-eap-demo/server/pom.xml +++ b/examples/as7-eap-demo/server/pom.xml @@ -118,7 +118,10 @@ com.h2database h2 - 1.3.161 + + + org.mongodb + mongo-java-driver junit diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java index 6c62007bb5..d5412cee08 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java @@ -819,6 +819,7 @@ public class RealmAdapter implements RealmModel { RelationshipQuery query = getRelationshipManager().createRelationshipQuery(SocialLinkRelationship.class); query.setParameter(SocialLinkRelationship.SOCIAL_PROVIDER, socialLink.getSocialProvider()); query.setParameter(SocialLinkRelationship.SOCIAL_USERNAME, socialLink.getSocialUsername()); + query.setParameter(SocialLinkRelationship.REALM, realm.getName()); List results = query.getResultList(); if (results.isEmpty()) { return null; @@ -850,6 +851,7 @@ public class RealmAdapter implements RealmModel { relationship.setUser(((UserAdapter)user).getUser()); relationship.setSocialProvider(socialLink.getSocialProvider()); relationship.setSocialUsername(socialLink.getSocialUsername()); + relationship.setRealm(realm.getName()); getRelationshipManager().add(relationship); } @@ -860,6 +862,7 @@ public class RealmAdapter implements RealmModel { relationship.setUser(((UserAdapter)user).getUser()); relationship.setSocialProvider(socialLink.getSocialProvider()); relationship.setSocialUsername(socialLink.getSocialUsername()); + relationship.setRealm(realm.getName()); getRelationshipManager().remove(relationship); } diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java index e9be9d467b..da8f04f01c 100755 --- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java +++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java @@ -3,6 +3,7 @@ package org.keycloak.models.picketlink.relationships; import org.picketlink.idm.model.AbstractAttributedType; import org.picketlink.idm.model.Attribute; import org.picketlink.idm.model.Relationship; +import org.picketlink.idm.model.annotation.AttributeProperty; import org.picketlink.idm.model.sample.User; import org.picketlink.idm.query.AttributeParameter; import org.picketlink.idm.query.RelationshipQueryParameter; @@ -21,6 +22,10 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re public static final AttributeParameter SOCIAL_PROVIDER = new AttributeParameter("socialProvider"); public static final AttributeParameter SOCIAL_USERNAME = new AttributeParameter("socialUsername"); + // realm is needed to allow searching as combination socialUsername+socialProvider may not be unique + // (Same user could have mapped same facebook account to username "foo" in "realm1" and to username "bar" in "realm2") + public static final AttributeParameter REALM = new AttributeParameter("realm"); + public static final RelationshipQueryParameter USER = new RelationshipQueryParameter() { @Override @@ -39,6 +44,7 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re this.user = user; } + @AttributeProperty public String getSocialProvider() { return (String)getAttribute("socialProvider").getValue(); } @@ -47,6 +53,7 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re setAttribute(new Attribute("socialProvider", socialProvider)); } + @AttributeProperty public String getSocialUsername() { return (String)getAttribute("socialUsername").getValue(); } @@ -54,4 +61,13 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re public void setSocialUsername(String socialProviderUserId) { setAttribute(new Attribute("socialUsername", socialProviderUserId)); } + + @AttributeProperty + public String getRealm() { + return (String)getAttribute("realm").getValue(); + } + + public void setRealm(String realm) { + setAttribute(new Attribute("realm", realm)); + } } diff --git a/pom.xml b/pom.xml index c2a58bcd10..9ab5c51f5b 100755 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,13 @@ 3.0.4.Final 1.0.0.Beta12 2.5.0.Beta6 + 2.11.2 + 3.1.1.GA + 1.0.1.Final + 3.6.6.Final + 1.3.161 + 1.6.1 + 1.6.1 http://keycloak.org @@ -177,7 +184,7 @@ org.jboss.logging jboss-logging - 3.1.1.GA + ${jboss.logging.version} junit @@ -187,7 +194,17 @@ org.hibernate.javax.persistence hibernate-jpa-2.0-api - 1.0.1.Final + ${hibernate.javax.persistence.version} + + + com.h2database + h2 + ${h2.version} + + + org.hibernate + hibernate-entitymanager + ${hibernate.entitymanager.version} com.google.api-client @@ -259,6 +276,42 @@ de.flapdoodle.embed.mongo 1.27 + + org.apache.jmeter + ApacheJMeter_java + 2.9 + + + dom4j + dom4j + ${dom4j.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + mysql + mysql-connector-java + 5.1.25 + + + + org.jboss.arquillian + arquillian-bom + 1.1.1.Final + pom + import + + + org.jboss.arquillian.extension + arquillian-drone-bom + 1.2.0.Beta1 + pom + import + @@ -340,6 +393,21 @@ false + + com.lazerycode.jmeter + jmeter-maven-plugin + 1.8.1 + + + com.lazerycode.jmeter + jmeter-analysis-maven-plugin + 1.0.4 + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + diff --git a/services/pom.xml b/services/pom.xml index 8696a5d1e0..b35bef4135 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -147,10 +147,12 @@ org.picketlink picketlink-common + provided org.mongodb mongo-java-driver + provided de.flapdoodle.embed @@ -170,13 +172,11 @@ com.h2database h2 - 1.3.161 test org.hibernate hibernate-entitymanager - 3.6.6.Final test diff --git a/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/NoSQLSession.java b/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/NoSQLSession.java index 838a6a4b60..71cd3a1e58 100644 --- a/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/NoSQLSession.java +++ b/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/NoSQLSession.java @@ -43,6 +43,10 @@ public class NoSQLSession implements KeycloakSession { @Override public RealmModel createRealm(String id, String name) { + if (getRealm(id) != null) { + throw new IllegalStateException("Realm with id '" + id + "' already exists"); + } + RealmData newRealm = new RealmData(); newRealm.setId(id); newRealm.setName(name); diff --git a/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/RealmAdapter.java b/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/RealmAdapter.java index 2165165d6a..85c9fcdb0d 100644 --- a/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/RealmAdapter.java +++ b/services/src/main/java/org/keycloak/services/models/nosql/keycloak/adapters/RealmAdapter.java @@ -273,6 +273,15 @@ public class RealmAdapter implements RealmModel { return new UserAdapter(userData, noSQL); } + // This method doesn't exists on interface actually + public void removeUser(String name) { + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("loginName", name) + .andCondition("realmId", getOid()) + .build(); + noSQL.removeObjects(UserData.class, query); + } + @Override public RoleAdapter getRole(String name) { NoSQLQuery query = noSQL.createQueryBuilder() @@ -659,6 +668,7 @@ public class RealmAdapter implements RealmModel { NoSQLQuery query = noSQL.createQueryBuilder() .andCondition("socialProvider", socialLink.getSocialProvider()) .andCondition("socialUsername", socialLink.getSocialUsername()) + .andCondition("realmId", getOid()) .build(); SocialLinkData socialLinkData = noSQL.loadSingleObject(SocialLinkData.class, query); @@ -696,6 +706,7 @@ public class RealmAdapter implements RealmModel { socialLinkData.setSocialProvider(socialLink.getSocialProvider()); socialLinkData.setSocialUsername(socialLink.getSocialUsername()); socialLinkData.setUserId(userData.getId()); + socialLinkData.setRealmId(getOid()); noSQL.saveObject(socialLinkData); } diff --git a/services/src/main/java/org/keycloak/services/models/nosql/keycloak/data/SocialLinkData.java b/services/src/main/java/org/keycloak/services/models/nosql/keycloak/data/SocialLinkData.java index 7cfe6f5096..46b1c2622d 100644 --- a/services/src/main/java/org/keycloak/services/models/nosql/keycloak/data/SocialLinkData.java +++ b/services/src/main/java/org/keycloak/services/models/nosql/keycloak/data/SocialLinkData.java @@ -15,6 +15,9 @@ public class SocialLinkData extends AbstractNoSQLObject { private String socialUsername; private String socialProvider; private String userId; + // realmId is needed to allow searching as combination socialUsername+socialProvider may not be unique + // (Same user could have mapped same facebook account to username "foo" in "realm1" and to username "bar" in "realm2") + private String realmId; @NoSQLField public String getSocialUsername() { @@ -42,4 +45,13 @@ public class SocialLinkData extends AbstractNoSQLObject { public void setUserId(String userId) { this.userId = userId; } + + @NoSQLField + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } } diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 7c5278d899..adb5aa3e7e 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -182,6 +182,10 @@ org.seleniumhq.selenium selenium-java + + org.apache.jmeter + ApacheJMeter_java + @@ -193,6 +197,17 @@ 1.6 + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + @@ -219,5 +234,156 @@ + + + performance-tests + + + + com.lazerycode.jmeter + jmeter-maven-plugin + + + jmeter-tests + verify + + jmeter + + + + + + org.keycloak + keycloak-testsuite + ${project.version} + test-jar + + + org.keycloak + keycloak-services + ${project.version} + + + org.jboss.resteasy + jaxrs-api + ${resteasy.version} + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version} + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + commons-io + commons-io + + + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + + + org.picketlink + picketlink-idm-impl + ${picketlink.version} + + + org.picketlink + picketlink-idm-simple-schema + ${picketlink.version} + + + org.picketlink + picketlink-config + ${picketlink.version} + + + org.mongodb + mongo-java-driver + ${mongo.driver.version} + + + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + ${hibernate.javax.persistence.version} + + + com.h2database + h2 + ${h2.version} + + + org.hibernate + hibernate-entitymanager + ${hibernate.entitymanager.version} + + + dom4j + dom4j + ${dom4j.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + mysql + mysql-connector-java + + + + + + + com.lazerycode.jmeter + jmeter-analysis-maven-plugin + + + jmeter-tests-analyze + verify + + analyze + + + ${project.build.directory}/jmeter/results/*.jtl + ${project.build.directory}/jmeter/results + false + + + + + + + + + + + + + + + + + + diff --git a/testsuite/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java b/testsuite/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java new file mode 100644 index 0000000000..bc38e6d7bd --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java @@ -0,0 +1,141 @@ +package org.keycloak.testsuite.performance; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient; +import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; +import org.apache.jmeter.samplers.SampleResult; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.KeycloakSessionFactory; +import org.keycloak.services.models.KeycloakTransaction; +import org.keycloak.services.models.picketlink.PicketlinkKeycloakSession; +import org.keycloak.services.resources.KeycloakApplication; + +/** + * @author Marek Posolda + */ +public class BaseJMeterPerformanceTest extends AbstractJavaSamplerClient { + + + private static FutureTask factoryProvider = new FutureTask(new Callable() { + + @Override + public KeycloakSessionFactory call() throws Exception { + return KeycloakApplication.buildSessionFactory(); + } + + }); + private static AtomicInteger counter = new AtomicInteger(); + + private KeycloakSessionFactory factory; + // private KeycloakSession identitySession; + private Worker worker; + private boolean setupSuccess = false; + + + // Executed once per JMeter thread + @Override + public void setupTest(JavaSamplerContext context) { + super.setupTest(context); + + worker = getWorker(); + + factory = getFactory(); + KeycloakSession identitySession = factory.createSession(); + KeycloakTransaction transaction = identitySession.getTransaction(); + transaction.begin(); + + int workerId = counter.getAndIncrement(); + try { + worker.setup(workerId, identitySession); + setupSuccess = true; + } finally { + if (setupSuccess) { + transaction.commit(); + } else { + transaction.rollback(); + } + identitySession.close(); + } + } + + private static KeycloakSessionFactory getFactory() { + factoryProvider.run(); + try { + return factoryProvider.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + private Worker getWorker() { + String workerClass = System.getProperty("keycloak.perf.workerClass"); + if (workerClass == null) { + throw new IllegalArgumentException("System property keycloak.perf.workerClass needs to be provided"); + } + + try { + Class workerClazz = Class.forName(workerClass); + return (Worker)workerClazz.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + @Override + public SampleResult runTest(JavaSamplerContext context) { + SampleResult result = new SampleResult(); + result.sampleStart(); + + if (!setupSuccess) { + getLogger().error("setupTest didn't executed successfully. Skipping"); + result.setResponseCode("500"); + result.sampleEnd(); + result.setSuccessful(true); + return result; + } + + KeycloakSession identitySession = factory.createSession(); + KeycloakTransaction transaction = identitySession.getTransaction(); + try { + transaction.begin(); + + worker.run(result, identitySession); + + result.setResponseCodeOK(); + transaction.commit(); + } catch (Exception e) { + getLogger().error("Error during worker processing", e); + result.setResponseCode("500"); + transaction.rollback(); + } finally { + result.sampleEnd(); + result.setSuccessful(true); + identitySession.close(); + } + + return result; + } + + + // Executed once per JMeter thread + @Override + public void teardownTest(JavaSamplerContext context) { + super.teardownTest(context); + + if (worker != null) { + worker.tearDown(); + } + + // TODO: Assumption is that tearDownTest is executed for each setupTest. Verify if it's always true... + if (counter.decrementAndGet() == 0) { + if (factory != null) { + factory.close(); + } + } + } +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java b/testsuite/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java new file mode 100644 index 0000000000..a3b37e091f --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java @@ -0,0 +1,101 @@ +package org.keycloak.testsuite.performance; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.models.ApplicationModel; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.RealmModel; + +/** + * @author Marek Posolda + */ +public class CreateRealmsWorker implements Worker { + + private static final Logger log = LoggingManager.getLoggerForClass(); + + private static final int NUMBER_OF_REALMS_IN_EACH_REPORT = 100; + + private static AtomicInteger realmCounter = new AtomicInteger(0); + + private int offset; + private int appsPerRealm; + private int rolesPerRealm; + private int defaultRolesPerRealm; + private int rolesPerApp; + private boolean createRequiredCredentials; + + @Override + public void setup(int workerId, KeycloakSession identitySession) { + offset = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.realms.offset", Integer.class); + appsPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.appsPerRealm", Integer.class); + rolesPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.rolesPerRealm", Integer.class); + defaultRolesPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.defaultRolesPerRealm", Integer.class); + rolesPerApp = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.rolesPerApp", Integer.class); + createRequiredCredentials = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.createRequiredCredentials", Boolean.class); + + realmCounter.compareAndSet(0, offset); + + StringBuilder logBuilder = new StringBuilder("Read setup: ") + .append("offset=" + offset) + .append(", appsPerRealm=" + appsPerRealm) + .append(", rolesPerRealm=" + rolesPerRealm) + .append(", defaultRolesPerRealm=" + defaultRolesPerRealm) + .append(", rolesPerApp=" + rolesPerApp) + .append(", createRequiredCredentials=" + createRequiredCredentials); + log.info(logBuilder.toString()); + } + + @Override + public void run(SampleResult result, KeycloakSession identitySession) { + int realmNumber = realmCounter.getAndIncrement(); + String realmName = PerfTestUtils.getRealmName(realmNumber); + RealmManager realmManager = new RealmManager(identitySession); + RealmModel realm = realmManager.createRealm(realmName, realmName); + + // Add roles + for (int i=1 ; i<=rolesPerRealm ; i++) { + realm.addRole(PerfTestUtils.getRoleName(realmNumber, i)); + } + + // Add default roles + for (int i=1 ; i<=defaultRolesPerRealm ; i++) { + realm.addDefaultRole(PerfTestUtils.getDefaultRoleName(realmNumber, i)); + } + + // Add applications + for (int i=1 ; i<=appsPerRealm ; i++) { + ApplicationModel application = realm.addApplication(PerfTestUtils.getApplicationName(realmNumber, i)); + for (int j=1 ; j<=rolesPerApp ; j++) { + application.addRole(PerfTestUtils.getApplicationRoleName(realmNumber, i, j)); + } + } + + // Add required credentials + if (createRequiredCredentials) { + realmManager.addRequiredCredential(realm, CredentialRepresentation.PASSWORD); + realmManager.addResourceRequiredCredential(realm, CredentialRepresentation.PASSWORD); + realmManager.addOAuthClientRequiredCredential(realm, CredentialRepresentation.PASSWORD); + realmManager.addRequiredCredential(realm, CredentialRepresentation.TOTP); + realmManager.addResourceRequiredCredential(realm, CredentialRepresentation.TOTP); + realmManager.addOAuthClientRequiredCredential(realm, CredentialRepresentation.TOTP); + realmManager.addRequiredCredential(realm, CredentialRepresentation.CLIENT_CERT); + realmManager.addResourceRequiredCredential(realm, CredentialRepresentation.CLIENT_CERT); + realmManager.addOAuthClientRequiredCredential(realm, CredentialRepresentation.CLIENT_CERT); + } + + log.info("Finished creation of realm " + realmName); + + int labelC = ((realmNumber - 1) / NUMBER_OF_REALMS_IN_EACH_REPORT) * NUMBER_OF_REALMS_IN_EACH_REPORT; + result.setSampleLabel("CreateRealms " + (labelC + 1) + "-" + (labelC + NUMBER_OF_REALMS_IN_EACH_REPORT)); + } + + @Override + public void tearDown() { + } + +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java b/testsuite/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java new file mode 100644 index 0000000000..f4279442d3 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java @@ -0,0 +1,120 @@ +package org.keycloak.testsuite.performance; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.RealmModel; +import org.keycloak.services.models.RoleModel; +import org.keycloak.services.models.SocialLinkModel; +import org.keycloak.services.models.UserCredentialModel; +import org.keycloak.services.models.UserModel; + +/** + * @author Marek Posolda + */ +public class CreateUsersWorker implements Worker { + + private static final Logger log = LoggingManager.getLoggerForClass(); + + private static final int NUMBER_OF_USERS_IN_EACH_REPORT = 5000; + + // Total number of users created during whole test + private static AtomicInteger totalUserCounter = new AtomicInteger(); + + // Adding users will always start from 1. Each worker thread needs to add users to single realm, which is dedicated just for this worker + private int userCounterInRealm = 0; + private String realmId; + + private int realmsOffset; + private boolean addBasicUserAttributes; + private boolean addDefaultRoles; + private boolean addPassword; + private int socialLinksPerUserCount; + + @Override + public void setup(int workerId, KeycloakSession identitySession) { + realmsOffset = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.realms.offset", Integer.class); + addBasicUserAttributes = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.addBasicUserAttributes", Boolean.class); + addDefaultRoles = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.addDefaultRoles", Boolean.class); + addPassword = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.addPassword", Boolean.class); + socialLinksPerUserCount = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.socialLinksPerUserCount", Integer.class); + + int realmNumber = realmsOffset + workerId; + realmId = PerfTestUtils.getRealmName(realmNumber); + + StringBuilder logBuilder = new StringBuilder("Read setup: ") + .append("realmsOffset=" + realmsOffset) + .append(", addBasicUserAttributes=" + addBasicUserAttributes) + .append(", addDefaultRoles=" + addDefaultRoles) + .append(", addPassword=" + addPassword) + .append(", socialLinksPerUserCount=" + socialLinksPerUserCount) + .append(", realmId=" + realmId); + log.info(logBuilder.toString()); + } + + @Override + public void run(SampleResult result, KeycloakSession identitySession) { + // We need to obtain realm first + RealmModel realm = identitySession.getRealm(realmId); + if (realm == null) { + throw new IllegalStateException("Realm '" + realmId + "' not found"); + } + + int userNumber = ++userCounterInRealm; + int totalUserNumber = totalUserCounter.incrementAndGet(); + + String username = PerfTestUtils.getUsername(userNumber); + + UserModel user = realm.addUser(username); + + // Add basic user attributes (NOTE: Actually backend is automatically upgraded during each setter call) + if (addBasicUserAttributes) { + user.setFirstName(username + "FN"); + user.setLastName(username + "LN"); + user.setEmail(username + "@email.com"); + } + + // Adding default roles of realm to user + if (addDefaultRoles) { + for (RoleModel role : realm.getDefaultRoles()) { + realm.grantRole(user, role); + } + } + + // Creating password (will be same as username) + if (addPassword) { + UserCredentialModel password = new UserCredentialModel(); + password.setType(CredentialRepresentation.PASSWORD); + password.setValue(username); + realm.updateCredential(user, password); + } + + // Creating some socialLinks + for (int i=0 ; iMarek Posolda + */ +public class PerfTestUtils { + + public static T readSystemProperty(String propertyName, Class expectedClass) { + String propAsString = System.getProperty(propertyName); + if (propAsString == null || propAsString.length() == 0) { + throw new IllegalArgumentException("Property '" + propertyName + "' not specified"); + } + + if (Integer.class.equals(expectedClass)) { + return expectedClass.cast(Integer.parseInt(propAsString)); + } else if (Boolean.class.equals(expectedClass)) { + return expectedClass.cast(Boolean.valueOf(propAsString)); + } else { + throw new IllegalArgumentException("Not supported type " + expectedClass); + } + } + + public static String getRealmName(int realmNumber) { + return "realm" + realmNumber; + } + + public static String getApplicationName(int realmNumber, int applicationNumber) { + return getRealmName(realmNumber) + "application" + applicationNumber; + } + + public static String getRoleName(int realmNumber, int roleNumber) { + return getRealmName(realmNumber) + "role" + roleNumber; + } + + public static String getDefaultRoleName(int realmNumber, int defaultRoleNumber) { + return getRealmName(realmNumber) + "defrole" + defaultRoleNumber; + } + + public static String getApplicationRoleName(int realmNumber, int applicationNumber, int roleNumber) { + return getApplicationName(realmNumber, applicationNumber) + "role" + roleNumber; + } + + public static String getUsername(int userNumber) { + return "user" + userNumber; + } +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java b/testsuite/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java new file mode 100644 index 0000000000..1439919512 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java @@ -0,0 +1,127 @@ +package org.keycloak.testsuite.performance; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.RealmModel; +import org.keycloak.services.models.SocialLinkModel; +import org.keycloak.services.models.UserModel; + +/** + * @author Marek Posolda + */ +public class ReadUsersWorker implements Worker { + + private static final Logger log = LoggingManager.getLoggerForClass(); + + private static final int NUMBER_OF_ITERATIONS_IN_EACH_REPORT = 5000; + + // Total number of iterations read during whole test + private static AtomicInteger totalIterationCounter = new AtomicInteger(); + + // Reading users will always start from 1. Each worker thread needs to read users to single realm, which is dedicated just for this worker + private int userCounterInRealm = 0; + + private int realmsOffset; + private int readUsersPerIteration; + private int countOfUsersPerRealm; + private boolean readRoles; + private boolean readScopes; + private boolean readPassword; + private boolean readSocialLinks; + private boolean searchBySocialLinks; + + private String realmId; + private int iterationNumber; + + @Override + public void setup(int workerId, KeycloakSession identitySession) { + realmsOffset = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.realms.offset", Integer.class); + readUsersPerIteration = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readUsersPerIteration", Integer.class); + countOfUsersPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.countOfUsersPerRealm", Integer.class); + readRoles = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readRoles", Boolean.class); + readScopes = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readScopes", Boolean.class); + readPassword = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readPassword", Boolean.class); + readSocialLinks = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readSocialLinks", Boolean.class); + searchBySocialLinks = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.searchBySocialLinks", Boolean.class); + + int realmNumber = realmsOffset + workerId; + realmId = PerfTestUtils.getRealmName(realmNumber); + + StringBuilder logBuilder = new StringBuilder("Read setup: ") + .append("realmsOffset=" + realmsOffset) + .append(", readUsersPerIteration=" + readUsersPerIteration) + .append(", countOfUsersPerRealm=" + countOfUsersPerRealm) + .append(", readRoles=" + readRoles) + .append(", readScopes=" + readScopes) + .append(", readPassword=" + readPassword) + .append(", readSocialLinks=" + readSocialLinks) + .append(", searchBySocialLinks=" + searchBySocialLinks) + .append(", realmId=" + realmId); + log.info(logBuilder.toString()); + } + + @Override + public void run(SampleResult result, KeycloakSession identitySession) { + // We need to obtain realm first + RealmModel realm = identitySession.getRealm(realmId); + if (realm == null) { + throw new IllegalStateException("Realm '" + realmId + "' not found"); + } + + int totalIterationNumber = totalIterationCounter.incrementAndGet(); + String lastUsername = null; + + for (int i=0 ; i countOfUsersPerRealm) { + userCounterInRealm = 1; + } + + String username = PerfTestUtils.getUsername(userCounterInRealm); + lastUsername = username; + + UserModel user = realm.getUser(username); + + // Read roles of user in realm + if (readRoles) { + realm.getRoleMappings(user); + } + + // Read scopes of user in realm + if (readScopes) { + realm.getScope(user); + } + + // Validate password (shoould be same as username) + if (readPassword) { + realm.validatePassword(user, username); + } + + // Read socialLinks of user + if (readSocialLinks) { + realm.getSocialLinks(user); + } + + // Try to search by social links + if (searchBySocialLinks) { + SocialLinkModel socialLink = new SocialLinkModel("facebook", username); + realm.getUserBySocialLink(socialLink); + } + } + + log.info("Finished iteration " + ++iterationNumber + " in ReadUsers test for " + realmId + " worker. Last read user " + lastUsername + " in realm: " + realmId); + + int labelC = ((totalIterationNumber - 1) / NUMBER_OF_ITERATIONS_IN_EACH_REPORT) * NUMBER_OF_ITERATIONS_IN_EACH_REPORT; + result.setSampleLabel("ReadUsers " + (labelC + 1) + "-" + (labelC + NUMBER_OF_ITERATIONS_IN_EACH_REPORT)); + } + + @Override + public void tearDown() { + } +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java b/testsuite/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java new file mode 100644 index 0000000000..966024ac73 --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java @@ -0,0 +1,72 @@ +package org.keycloak.testsuite.performance; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jorphan.logging.LoggingManager; +import org.apache.log.Logger; +import org.keycloak.services.models.KeycloakSession; +import org.keycloak.services.models.RealmModel; +import org.keycloak.services.models.SocialLinkModel; +import org.keycloak.services.models.UserModel; +import org.keycloak.services.models.nosql.keycloak.adapters.RealmAdapter; +import org.keycloak.services.resources.KeycloakApplication; + +/** + * @author Marek Posolda + */ +public class RemoveUsersWorker implements Worker { + + private static final Logger log = LoggingManager.getLoggerForClass(); + + private static final int NUMBER_OF_USERS_IN_EACH_REPORT = 5000; + + // Total number of users removed during whole test + private static AtomicInteger totalUserCounter = new AtomicInteger(); + + // Removing users will always start from 1. Each worker thread needs to add users to single realm, which is dedicated just for this worker + private int userCounterInRealm = 0; + private RealmModel realm; + + private int realmsOffset; + + @Override + public void setup(int workerId, KeycloakSession identitySession) { + realmsOffset = PerfTestUtils.readSystemProperty("keycloak.perf.removeUsers.realms.offset", Integer.class); + + int realmNumber = realmsOffset + workerId; + String realmId = PerfTestUtils.getRealmName(realmNumber); + realm = identitySession.getRealm(realmId); + if (realm == null) { + throw new IllegalStateException("Realm '" + realmId + "' not found"); + } + + log.info("Read setup: realmsOffset=" + realmsOffset); + } + + @Override + public void run(SampleResult result, KeycloakSession identitySession) { + int userNumber = ++userCounterInRealm; + int totalUserNumber = totalUserCounter.incrementAndGet(); + + String username = PerfTestUtils.getUsername(userNumber); + + // TODO: Not supported in model actually. We support operation just in MongoDB + // UserModel user = realm.removeUser(username); + if (KeycloakApplication.SESSION_FACTORY_MONGO.equals(System.getProperty(KeycloakApplication.SESSION_FACTORY))) { + RealmAdapter mongoRealm = (RealmAdapter)realm; + mongoRealm.removeUser(username); + } else { + throw new IllegalArgumentException("Actually removing of users is supported just for MongoDB"); + } + + log.info("Finished removing of user " + username + " in realm: " + realm.getId()); + + int labelC = ((totalUserNumber - 1) / NUMBER_OF_USERS_IN_EACH_REPORT) * NUMBER_OF_USERS_IN_EACH_REPORT; + result.setSampleLabel("ReadUsers " + (labelC + 1) + "-" + (labelC + NUMBER_OF_USERS_IN_EACH_REPORT)); + } + + @Override + public void tearDown() { + } +} diff --git a/testsuite/src/test/java/org/keycloak/testsuite/performance/Worker.java b/testsuite/src/test/java/org/keycloak/testsuite/performance/Worker.java new file mode 100644 index 0000000000..6060f49a3f --- /dev/null +++ b/testsuite/src/test/java/org/keycloak/testsuite/performance/Worker.java @@ -0,0 +1,19 @@ +package org.keycloak.testsuite.performance; + +import org.apache.jmeter.samplers.SampleResult; +import org.apache.log.Logger; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.models.KeycloakSession; + +/** + * @author Marek Posolda + */ +public interface Worker { + + void setup(int workerId, KeycloakSession identitySession); + + void run(SampleResult result, KeycloakSession identitySession); + + void tearDown(); + +} diff --git a/testsuite/src/test/jmeter/jmeter.properties b/testsuite/src/test/jmeter/jmeter.properties new file mode 100644 index 0000000000..39b6ce46ab --- /dev/null +++ b/testsuite/src/test/jmeter/jmeter.properties @@ -0,0 +1,20 @@ +#Thu Mar 07 18:46:04 BRT 2013 +not_in_menu=HTML Parameter Mask,HTTP User Parameter Modifier +xml.parser=org.apache.xerces.parsers.SAXParser +cookies=cookies +wmlParser.className=org.apache.jmeter.protocol.http.parser.RegexpHTMLParser +HTTPResponse.parsers=htmlParser wmlParser +remote_hosts=127.0.0.1 +system.properties=system.properties +beanshell.server.file=../extras/startup.bsh +log_level.jmeter.junit=DEBUG +sampleresult.timestamp.start=true +jmeter.laf.mac=System +log_level.jorphan=INFO +classfinder.functions.contain=.functions. +user.properties=user.properties +wmlParser.types=text/vnd.wap.wml +log_level.jmeter=DEBUG +classfinder.functions.notContain=.gui. +htmlParser.types=text/html application/xhtml+xml application/xml text/xml +upgrade_properties=/bin/upgrade.properties \ No newline at end of file diff --git a/testsuite/src/test/jmeter/mongo_test.jmx b/testsuite/src/test/jmeter/mongo_test.jmx new file mode 100644 index 0000000000..b96dcf36d1 --- /dev/null +++ b/testsuite/src/test/jmeter/mongo_test.jmx @@ -0,0 +1,39 @@ + + + + + + false + false + + + + + + + + continue + + false + 10 + + 1 + 0 + 1362689985000 + 1362689985000 + false + + + + + + + + + + org.keycloak.testsuite.performance.BaseJMeterPerformanceTest + + + + + \ No newline at end of file diff --git a/testsuite/src/test/jmeter/system.properties b/testsuite/src/test/jmeter/system.properties new file mode 100644 index 0000000000..26d24aab9b --- /dev/null +++ b/testsuite/src/test/jmeter/system.properties @@ -0,0 +1,79 @@ +## Choose implementation of KeycloakSessionFactory +# keycloak.sessionFactory=picketlink +keycloak.sessionFactory=mongo + +## Configure JPA (just hbm2ddl schema configurable here. Rest of the stuff in META-INF/persistence.xml) +keycloak.jpa.hbm2ddl.auto=create +# keycloak.jpa.hbm2ddl.auto=update + + +## Configure MongoDB (Useful just when keycloak.sessionFactory=mongo) +keycloak.mongodb.host=localhost +keycloak.mongodb.port=27017 +keycloak.mongodb.databaseName=keycloakPerfTest +# Should be DB dropped at startup of the test? +keycloak.mongodb.dropDatabaseOnStartup=true + + +## Specify Keycloak worker class +keycloak.perf.workerClass=org.keycloak.testsuite.performance.CreateRealmsWorker +# keycloak.perf.workerClass=org.keycloak.testsuite.performance.CreateUsersWorker +# keycloak.perf.workerClass=org.keycloak.testsuite.performance.ReadUsersWorker +# keycloak.perf.workerClass=org.keycloak.testsuite.performance.RemoveUsersWorker + + +## Properties for CreateRealms test. This test is used to create some realms. +# Each iteration of single worker thread will add one realm and it will add some roles, defaultRoles, credentials and applications to it +# Offset where to start creating realms. Count (total number of realms to create) is configurable as number of JMeter threads*loopCount +# For example: if offset==1 and in JMeter properties we have LoopController.loops=10 and num_threads=2 then we will create 20 realms in total and we will create realms "realm1" - "realm10" +# NOTE: Count (total number of realms to create) is configurable as number of JMeter threads*loopCount +keycloak.perf.createRealms.realms.offset=1 +# Count of apps per each realm (For example if count=5, we will create apps like "realm1app1" - "realm1app5" for realm "realm1" +# and similarly for all other created realms) +keycloak.perf.createRealms.appsPerRealm=5 +# Count of roles per each realm (For example if count=5, we will create roles like "realm1role1" - "realm1role5" for realm "realm1" +# and similarly for all other created realms) +keycloak.perf.createRealms.rolesPerRealm=5 +# Count of default roles per each realm (For example if count=2, we will create roles like "realm1defrole1" and "realm1defrole2" +# for realm "realm1" and similarly for all other created realms) +keycloak.perf.createRealms.defaultRolesPerRealm=2 +# Count of roles per each application (For example if count=3 we will have roles "realm1app1role1" - "realm1app1role3" for realm=1 and application=1 +# (if realmsCount=10, appsPerRealm=5 it will be 150 application roles totally) +keycloak.perf.createRealms.rolesPerApp=3 +# Whether to create required credentials in each realm (If true, we will create "password", "totp" and client-certificate) +keycloak.perf.createRealms.createRequiredCredentials=true + + +## Properties for CreateUsers test. This test is used to create some users +# Each iteration of single worker thread will add one user and it will add some default roles, passwords and bind him with some social accounts +# Each worker will use separate realm dedicated just for him, so each worker will create user1, user2, ... , userN . N (number of users to create per realm) +# is configurable in JMeter configuration as loopCount. Total number of created users for whole test will be threads*loopCount +# NOTE: For each thread, the corresponding realm must already exists +# Realm where to start creating users +keycloak.perf.createUsers.realms.offset=1 +# Whether to add basic attributes like firstName/lastName/email to each user +keycloak.perf.createUsers.addBasicUserAttributes=true +# Whether to add all default roles of realm to this user +keycloak.perf.createUsers.addDefaultRoles=true +# Whether to add password to this user +keycloak.perf.createUsers.addPassword=true +# Number of social links to create for each user. Possible values are 0, 1, 2, 3 (For 3 it will create Facebook, Twitter and Google) +keycloak.perf.createUsers.socialLinksPerUserCount=0 + + +## Properties for ReadUsers test. This test is used to read some users from DB and alternatively read some of his properties (passwords, roles, scopes, socialLinks) +keycloak.perf.readUsers.realms.offset=1 +# Number of read users in each iteration +keycloak.perf.readUsers.readUsersPerIteration=5 +# Number of users to read in each realm. After reading all 2000 users, reading will start again from user1 +keycloak.perf.readUsers.countOfUsersPerRealm=2000 +keycloak.perf.readUsers.readRoles=true +keycloak.perf.readUsers.readScopes=true +keycloak.perf.readUsers.readPassword=true +keycloak.perf.readUsers.readSocialLinks=false +keycloak.perf.readUsers.searchBySocialLinks=false + + +## Properties for RemoveUsers worker. This test is used to remove some users from DB (and all their stuff actually) +# Similarly like in CreateUsers test, each worker works just with one realm. Number of removed users depends on JMeter property loopCount +keycloak.perf.removeUsers.realms.offset=1 diff --git a/testsuite/src/test/resources/META-INF/persistence-performance.xml b/testsuite/src/test/resources/META-INF/persistence-performance.xml new file mode 100644 index 0000000000..1dff64183f --- /dev/null +++ b/testsuite/src/test/resources/META-INF/persistence-performance.xml @@ -0,0 +1,40 @@ + + + org.hibernate.ejb.HibernatePersistence + + org.picketlink.idm.jpa.model.sample.simple.AttributedTypeEntity + org.picketlink.idm.jpa.model.sample.simple.AccountTypeEntity + org.picketlink.idm.jpa.model.sample.simple.RoleTypeEntity + org.picketlink.idm.jpa.model.sample.simple.GroupTypeEntity + org.picketlink.idm.jpa.model.sample.simple.IdentityTypeEntity + org.picketlink.idm.jpa.model.sample.simple.RelationshipTypeEntity + org.picketlink.idm.jpa.model.sample.simple.RelationshipIdentityTypeEntity + org.picketlink.idm.jpa.model.sample.simple.PartitionTypeEntity + org.picketlink.idm.jpa.model.sample.simple.PasswordCredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.DigestCredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.X509CredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.OTPCredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.AttributeTypeEntity + org.keycloak.services.models.picketlink.mappings.RealmEntity + org.keycloak.services.models.picketlink.mappings.ApplicationEntity + + true + + + + + + + + + + + + + + + + \ No newline at end of file