From 548ab4f78c6b83778c4168c037e9e4f7ed055ba9 Mon Sep 17 00:00:00 2001 From: Marko Strukelj Date: Tue, 20 Feb 2018 17:17:13 +0100 Subject: [PATCH] KEYCLOAK-6514 Common approach to writing performance tests --- testsuite/performance/README.log-tool.md | 2 +- testsuite/performance/README.md | 20 +- testsuite/performance/tests/pom.xml | 2 +- .../org/keycloak/performance/TestConfig.java | 2 +- .../tests/src/test/scala/Engine.scala | 4 +- .../test/scala/examples/SimpleExample4.scala | 1 + .../AdminConsoleScenarioBuilder.scala | 638 ++++++++++++++++++ .../keycloak/AdminConsoleSimulation.scala | 79 +-- .../AdminConsoleSimulationHelper.scala | 39 -- .../keycloak/BasicOIDCScenarioBuilder.scala | 175 +++++ .../scala/keycloak/BasicOIDCSimulation.scala | 61 ++ .../scala/keycloak/DefaultSimulation.scala | 161 ----- .../scala/keycloak/SimulationsHelper.scala | 557 --------------- 13 files changed, 924 insertions(+), 817 deletions(-) create mode 100644 testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleScenarioBuilder.scala delete mode 100644 testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulationHelper.scala create mode 100644 testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCScenarioBuilder.scala create mode 100644 testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCSimulation.scala delete mode 100644 testsuite/performance/tests/src/test/scala/keycloak/DefaultSimulation.scala delete mode 100644 testsuite/performance/tests/src/test/scala/keycloak/SimulationsHelper.scala diff --git a/testsuite/performance/README.log-tool.md b/testsuite/performance/README.log-tool.md index 25c85b1a50..12ae5ff766 100644 --- a/testsuite/performance/README.log-tool.md +++ b/testsuite/performance/README.log-tool.md @@ -7,7 +7,7 @@ Perform the usual test run: mvn verify -Pteardown mvn verify -Pprovision mvn verify -Pgenerate-data -Ddataset=100users -Dimport.workers=10 -DhashIterations=100 -mvn verify -Ptest -Ddataset=100users -DrunUsers=200 -DrampUpPeriod=10 -DuserThinkTime=0 -DbadLoginAttempts=1 -DrefreshTokenCount=1 -DnumOfIterations=3 +mvn verify -Ptest -Ddataset=100users -DrunUsers=200 -DrampUpPeriod=10 -DuserThinkTime=0 -DbadLoginAttempts=1 -DrefreshTokenCount=1 -DsteadyLoadPeriod=10 ``` Now analyze the generated simulation.log (adjust LOG_DIR, FROM, and TO): diff --git a/testsuite/performance/README.md b/testsuite/performance/README.md index 52e58b17c4..77a58a925d 100644 --- a/testsuite/performance/README.md +++ b/testsuite/performance/README.md @@ -27,7 +27,7 @@ mvn clean install # Make sure your Docker daemon is running THEN mvn verify -Pprovision mvn verify -Pgenerate-data -Ddataset=100u -DnumOfWorkers=10 -DhashIterations=100 -mvn verify -Ptest -Ddataset=100u -DrunUsers=200 -DrampUpPeriod=10 -DuserThinkTime=0 -DbadLoginAttempts=1 -DrefreshTokenCount=1 -DnumOfIterations=3 +mvn verify -Ptest -Ddataset=100u -DrunUsers=200 -DrampUpPeriod=10 -DuserThinkTime=0 -DbadLoginAttempts=1 -DrefreshTokenCount=1 -DsteadyLoadPeriod=10 ``` @@ -139,7 +139,7 @@ Usage: `mvn verify -Ptest[,cluster] [-DtestParameter=value]`. | Parameter | Description | Default Value | | --- | --- | --- | -| `gatling.simulationClass` | Classname of the simulation to be run. | `keycloak.DefaultSimulation` | +| `gatling.simulationClass` | Classname of the simulation to be run. | `keycloak.BasicOIDCSimulation` | | `dataset` | Name of the dataset to use. (Individual dataset properties can be overridden with `-Ddataset.property=value`.) | `default` | | `runUsers` | Number of users for the simulation run. | `1` | | `rampUpPeriod` | Period during which the users will be ramped up. (seconds) | `0` | @@ -149,7 +149,7 @@ Usage: `mvn verify -Ptest[,cluster] [-DtestParameter=value]`. | `userThinkTime` | Pause between individual scenario steps. | `5` | | `refreshTokenPeriod`| Period after which token should be refreshed. | `10` | -#### Addtional Parameters of `keycloak.DefaultSimulation` +#### Addtional Parameters of `keycloak.BasicOIDCSimulation` | Parameter | Description | Default Value | | --- | --- | --- | @@ -159,7 +159,7 @@ Usage: `mvn verify -Ptest[,cluster] [-DtestParameter=value]`. Example: -`mvn verify -Ptest -Dgatling.simulationClass=keycloak.AdminSimulation -Ddataset=100u -DrunUsers=1 -DsteadyLoadPeriod=30 -DuserThinkTime=0 -DrefreshTokenPeriod=15` +`mvn verify -Ptest -Dgatling.simulationClass=keycloak.AdminConsoleSimulation -Ddataset=100u -DrunUsers=1 -DsteadyLoadPeriod=30 -DuserThinkTime=0 -DrefreshTokenPeriod=15` ## Monitoring @@ -246,12 +246,16 @@ Thus, it's best to download and install [this SDK version](http://scala-lang.org Open Preferences in IntelliJ. Type 'plugins' in the search box. In the right pane click on 'Install JetBrains plugin'. Type 'scala' in the search box, and click Install button of the Scala plugin. -#### Run DefaultSimulation from IntelliJ +#### Run BasicOIDCSimulation from IntelliJ -In ProjectExplorer find Engine object (you can use ctrl-N / cmd-O). Right click on class name and select Run or Debug like for -JUnit tests. +Make sure that `performance` maven profile is enabled for IDEA to treat `performance` directory as a project module. -You'll have to create a test profile, and set 'VM options' with -Dkey=value to override default configuration values in TestConfig class. +You may also need to rebuild the module in IDEA for scala objects to become available. + +Then find Engine object In ProjectExplorer (you can use ctrl-N / cmd-O). Right click on class name and select Run or Debug as if it was +a JUnit tests. + +You'll have to edit a test configuration, and set 'VM options' to a list of -Dkey=value pairs to override default configuration values in TestConfig class. Make sure to set 'Use classpath of module' to 'performance-test'. diff --git a/testsuite/performance/tests/pom.xml b/testsuite/performance/tests/pom.xml index e12cdbafae..117bd80149 100644 --- a/testsuite/performance/tests/pom.xml +++ b/testsuite/performance/tests/pom.xml @@ -93,7 +93,7 @@ 3.2.2 3.3.0.Final - keycloak.DefaultSimulation + keycloak.BasicOIDCSimulation true diff --git a/testsuite/performance/tests/src/main/java/org/keycloak/performance/TestConfig.java b/testsuite/performance/tests/src/main/java/org/keycloak/performance/TestConfig.java index 4fb33b71f2..a0fbf2f201 100644 --- a/testsuite/performance/tests/src/main/java/org/keycloak/performance/TestConfig.java +++ b/testsuite/performance/tests/src/main/java/org/keycloak/performance/TestConfig.java @@ -68,7 +68,7 @@ public class TestConfig { public static final long rampDownPeriodStartTime = simulationStartTime + (rampUpPeriod + steadyLoadPeriod) * 1000; // - // Settings used by DefaultSimulation to control behavior specific to DefaultSimulation + // Settings used by BasicOIDCSimulation to control behavior specific to BasicOIDCSimulation // public static final int badLoginAttempts = Integer.getInteger("badLoginAttempts", 0); public static final int refreshTokenCount = Integer.getInteger("refreshTokenCount", 0); diff --git a/testsuite/performance/tests/src/test/scala/Engine.scala b/testsuite/performance/tests/src/test/scala/Engine.scala index d71126ba3c..c55af4487d 100644 --- a/testsuite/performance/tests/src/test/scala/Engine.scala +++ b/testsuite/performance/tests/src/test/scala/Engine.scala @@ -4,8 +4,8 @@ import io.gatling.core.config.GatlingPropertiesBuilder object Engine extends App { - val sim = classOf[keycloak.DefaultSimulation] - //val sim = classOf[keycloak.AdminSimulation] + val sim = classOf[keycloak.BasicOIDCSimulation] + //val sim = classOf[keycloak.AdminConsoleSimulation] val props = new GatlingPropertiesBuilder props.dataDirectory(IDEPathHelper.dataDirectory.toString) diff --git a/testsuite/performance/tests/src/test/scala/examples/SimpleExample4.scala b/testsuite/performance/tests/src/test/scala/examples/SimpleExample4.scala index c829fa4480..f15f75902f 100644 --- a/testsuite/performance/tests/src/test/scala/examples/SimpleExample4.scala +++ b/testsuite/performance/tests/src/test/scala/examples/SimpleExample4.scala @@ -24,6 +24,7 @@ class SimpleExample4 extends Simulation { .exec(account) setUp( + // rather than starting all 100 users at once, increase the count over a period of 10 seconds scn.inject(rampUsers(100) over 10).protocols(httpConf) ) } diff --git a/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleScenarioBuilder.scala b/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleScenarioBuilder.scala new file mode 100644 index 0000000000..0bdb140dd5 --- /dev/null +++ b/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleScenarioBuilder.scala @@ -0,0 +1,638 @@ +package keycloak + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import keycloak.AdminConsoleScenarioBuilder._ + +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +import io.gatling.core.pause.Normal +import io.gatling.http.request.StringBody +import org.jboss.perf.util.Util +import org.jboss.perf.util.Util.randomUUID +import org.keycloak.gatling.Utils.{urlEncodedRoot, urlencode} +import org.keycloak.performance.TestConfig + + +/** + * @author Marko Strukelj + */ + +object AdminConsoleScenarioBuilder { + + val UI_HEADERS = Map( + "Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests" -> "1") + + val ACCEPT_JSON = Map("Accept" -> "application/json") + val ACCEPT_ALL = Map("Accept" -> "*/*") + val AUTHORIZATION = Map("Authorization" -> "Bearer ${accessToken}") + + val APP_URL = "${keycloakServer}/admin/master/console/" + val DATE_FMT = DateTimeFormatter.RFC_1123_DATE_TIME + + + def getRandomUser() : String = { + "user_" + (Util.random.nextDouble() * TestConfig.usersPerRealm).toInt + } + + def needTokenRefresh(sess: Session): Boolean = { + val lastRefresh = sess("accessTokenRefreshTime").as[Long] + + // 5 seconds before expiry is time to refresh + lastRefresh + sess("expiresIn").as[String].toInt * 1000 - 5000 < System.currentTimeMillis() || + // or if refreshTokenPeriod is set force refresh even if not necessary + (TestConfig.refreshTokenPeriod > 0 && + lastRefresh + TestConfig.refreshTokenPeriod * 1000 < System.currentTimeMillis()) + } + +} + +class AdminConsoleScenarioBuilder { + + var chainBuilder = exec(s => { + val realm = TestConfig.randomRealmsIterator().next() + val serverUrl = TestConfig.serverUrisList.get(0) + s.setAll( + "keycloakServer" -> serverUrl, + "keycloakServerUrlEncoded" -> urlencode(serverUrl), + "keycloakServerRootEncoded" -> urlEncodedRoot(serverUrl), + "state" -> randomUUID(), + "nonce" -> randomUUID(), + "randomClientId" -> ("client_" + randomUUID()), + "realm" -> realm, + "username" -> "admin", + "password" -> "admin", + "clientId" -> "security-admin-console" + ) + }).exitHereIfFailed + + + + def thinkPause() : AdminConsoleScenarioBuilder = { + chainBuilder = chainBuilder.pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) + this + } + + def needTokenRefresh(sess: Session): Boolean = { + val lastRefresh = sess("accessTokenRefreshTime").as[Long] + + // 5 seconds before expiry is time to refresh + lastRefresh + sess("expiresIn").as[String].toInt * 1000 - 5000 < System.currentTimeMillis() || + // or if refreshTokenPeriod is set force refresh even if not necessary + (TestConfig.refreshTokenPeriod > 0 && + lastRefresh + TestConfig.refreshTokenPeriod * 1000 < System.currentTimeMillis()) + } + + def refreshTokenIfExpired() : AdminConsoleScenarioBuilder = { + chainBuilder = chainBuilder + .doIf(s => needTokenRefresh(s)) { + exec(http("JS Adapter Token - Refresh tokens") + .post("/auth/realms/master/protocol/openid-connect/token") + .headers(ACCEPT_ALL) + .formParam("grant_type", "refresh_token") + .formParam("refresh_token", "${refreshToken}") + .formParam("client_id", "security-admin-console") + .check(status.is(200), + jsonPath("$.access_token").saveAs("accessToken"), + jsonPath("$.refresh_token").saveAs("refreshToken"), + jsonPath("$.expires_in").saveAs("expiresIn"), + header("Date").saveAs("tokenTime"))) + + .exec(s => { + s.set("accessTokenRefreshTime", ZonedDateTime.parse(s("tokenTime").as[String], DATE_FMT).toEpochSecond * 1000) + }) + } + this + } + + def openAdminConsoleHome() : AdminConsoleScenarioBuilder = { + chainBuilder = chainBuilder + .exec(http("Console Home") + .get("/auth/admin/") + .headers(UI_HEADERS) + .check(status.is(302)) + .resources( + http("Console Redirect") + .get("/auth/admin/master/console/") + .headers(UI_HEADERS) + .check(status.is(200), regex("").saveAs("resourceVersion")), + http("Console REST - Config") + .get("/auth/admin/master/console/config") + .headers(ACCEPT_JSON) + .check(status.is(200)) + )) + this + } + + def loginThroughLoginForm() : AdminConsoleScenarioBuilder = { + chainBuilder = chainBuilder + .exec(http("JS Adapter Auth - Login Form Redirect") + .get("/auth/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=${keycloakServerUrlEncoded}%2Fadmin%2Fmaster%2Fconsole%2F&state=${state}&nonce=${nonce}&response_mode=fragment&response_type=code&scope=openid") + .headers(UI_HEADERS) + .check(status.is(200), regex("action=\"([^\"]*)\"").find.transform(_.replaceAll("&", "&")).saveAs("login-form-uri"))) + .exitHereIfFailed + + // thinkPause + thinkPause() + + // Successful login + chainBuilder = chainBuilder + .exec(http("Login Form - Submit Correct Credentials") + .post("${login-form-uri}") + .formParam("username", "${username}") + .formParam("password", "${password}") + .formParam("login", "Log in") + .check(status.is(302), + header("Location").saveAs("login-redirect"), + headerRegex("Location", "code=([^&]+)").saveAs("code"))) + // TODO store AUTH_SESSION_ID cookie for use with oauth.authorize? + .exitHereIfFailed + + .exec(http("Console Redirect") + .get("/auth/admin/master/console/") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + http("Console REST - Config") + .get("/auth/admin/master/console/config") + .headers(ACCEPT_JSON) + .check(status.is(200)), + + http("JS Adapter Token - Exchange code for tokens") + .post("/auth/realms/master/protocol/openid-connect/token") + .headers(ACCEPT_ALL) + .formParam("code", "${code}") + .formParam("grant_type", "authorization_code") + .formParam("client_id", "security-admin-console") + .formParam("redirect_uri", APP_URL) + .check(status.is(200), + jsonPath("$.access_token").saveAs("accessToken"), + jsonPath("$.refresh_token").saveAs("refreshToken"), + jsonPath("$.expires_in").saveAs("expiresIn"), + header("Date").saveAs("tokenTime")), + + http("Console REST - messages.json") + .get("/auth/admin/master/console/messages.json?lang=en") + .headers(ACCEPT_JSON) + .check(status.is(200)), + + // iframe status listener + // TODO: properly set Referer + http("IFrame Status Init") + .get("/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?client_id=security-admin-console&origin=${keycloakServerRootEncoded}") // ${keycloakServerUrlEncoded} + .headers(ACCEPT_ALL) // ++ Map("Referer" -> "/auth/realms/master/protocol/openid-connect/login-status-iframe.html?version=3.3.0.cr1-201708011508") ${resourceVersion} + .check(status.is(204)) + ) + ) + .exec(s => { + // How to not have to duplicate this block of code? + s.set("accessTokenRefreshTime", ZonedDateTime.parse(s("tokenTime").as[String], DATE_FMT).toEpochSecond * 1000) + }) + .exec(http("Console REST - whoami") + .get("/auth/admin/master/console/whoami") + .headers(ACCEPT_JSON ++ AUTHORIZATION) + .check(status.is(200))) + + .exec(http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200))) + + .exec(http("Console REST - serverinfo") + .get("/auth/admin/serverinfo") + .headers(AUTHORIZATION) + .check(status.is(200))) + + .exec(http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200))) + + .exec(http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200))) + + .exec(http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200))) + this + } + + def openRealmSettings() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder.exec(http("Console Realm Settings") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/realm-detail.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console - kc-tabs-realm.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-realm.html") + //.headers(UI_HEADERS ++ Map("Referer" -> "")) // TODO fix referer + .headers(UI_HEADERS) + .check(status.is(200)), + + http("Console - kc-menu.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-menu.html") + //.headers(UI_HEADERS ++ Map("Referer" -> "")) // TODO fix referer + .headers(UI_HEADERS) + .check(status.is(200)), + + // request fonts for css also set referer + http("OpenSans-Semibold-webfont.woff") + .get("/auth/resources/${resourceVersion}/admin/keycloak/lib/patternfly/fonts/OpenSans-Semibold-webfont.woff") + .headers(UI_HEADERS) + .check(status.is(200)), + + http("OpenSans-Bold-webfont.woff") + .get("/auth/resources/${resourceVersion}/admin/keycloak/lib/patternfly/fonts/OpenSans-Bold-webfont.woff") + .headers(UI_HEADERS) + .check(status.is(200)), + + http("OpenSans-Light-webfont.woff") + .get("/auth/resources/${resourceVersion}/admin/keycloak/lib/patternfly/fonts/OpenSans-Light-webfont.woff") + .headers(UI_HEADERS) + .check(status.is(200)) + ) + ) + .exitHereIfFailed + this + } + + def openClients() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console - client-list.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/client-list.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + http("Console - kc-paging.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-paging.html") + .headers(UI_HEADERS) + .check(status.is(200)), + http("Console REST - ${realm}/clients") + .get("/auth/admin/realms/${realm}/clients?viewableOnly=true") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + ) + this + } + + def openCreateNewClient() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console - create-client.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/create-client.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + http("Console REST - ${realm}/clients") + .get("/auth/admin/realms/${realm}/clients") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + ) + this + } + + def submitNewClient() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console REST - ${realm}/clients POST") + .post("/auth/admin/realms/${realm}/clients") + .headers(AUTHORIZATION) + .header("Content-Type", "application/json") + .body(StringBody( + """ + {"enabled":true,"attributes":{},"redirectUris":[],"clientId":"${randomClientId}","rootUrl":"http://localhost:8081/myapp","protocol":"openid-connect"} + """.stripMargin)) + .check(status.is(201), headerRegex("Location", "\\/([^\\/]+)$").saveAs("idOfClient"))) + + .exec(http("Console REST - ${realm}/clients/ID") + .get("/auth/admin/realms/${realm}/clients/${idOfClient}") + .headers(AUTHORIZATION) + .check(status.is(200), bodyString.saveAs("clientJson")) + .resources( + http("Console REST - ${realm}/clients") + .get("/auth/admin/realms/${realm}/clients") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/client-templates") + .get("/auth/admin/realms/${realm}/client-templates") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + ) + this + } + + def updateClient() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder.exec(s => { + s.set("updateClientJson", s("clientJson").as[String].replace("\"publicClient\":false", "\"publicClient\":true")) + }) + .exec(http("Console REST - ${realm}/clients/ID PUT") + .put("/auth/admin/realms/${realm}/clients/${idOfClient}") + .headers(AUTHORIZATION) + .header("Content-Type", "application/json") + .body(StringBody("${updateClientJson}")) + .check(status.is(204))) + + .exec(http("Console REST - ${realm}/clients/ID") + .get("/auth/admin/realms/${realm}/clients/${idOfClient}") + .headers(AUTHORIZATION) + .check(status.is(200), bodyString.saveAs("clientJson")) + .resources( + http("Console REST - ${realm}/clients") + .get("/auth/admin/realms/${realm}/clients") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/client-templates") + .get("/auth/admin/realms/${realm}/client-templates") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + ) + this + } + + def openClientDetails() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console - client-detail.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/client-detail.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + http("Console REST - ${realm}/client-templates") + .get("/auth/admin/realms/${realm}/client-templates") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/clients") + .get("/auth/admin/realms/${realm}/clients") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/clients/ID") + .get("/auth/admin/realms/${realm}/clients/${idOfClient}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console - kc-tabs-client.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-client.html") + .headers(UI_HEADERS) + .check(status.is(200)) + ) + ) + this + } + + def openUsers() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console - user-list.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/user-list.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + http("Console - kc-tabs-users.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-users.html") + .headers(UI_HEADERS) + .check(status.is(200)) + ) + ) + this + } + + def viewAllUsers() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console REST - ${realm}/users") + .get("/auth/admin/realms/${realm}/users?first=0&max=20") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + this + } + + def viewTenPagesOfUsers() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .repeat(10, "i") { + exec(s => s.set("offset", s("i").as[Int] * 20)) + .pause(1) + .exec(http("Console REST - ${realm}/users?first=${offset}") + .get("/auth/admin/realms/${realm}/users?first=${offset}&max=20") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + } + this + } + + def find20Users() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console REST - ${realm}/users?first=0&max=20&search=user") + .get("/auth/admin/realms/${realm}/users?first=0&max=20&search=user") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + this + } + + def findUnlimitedUsers() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console REST - ${realm}/users?search=user") + .get("/auth/admin/realms/${realm}/users?search=user") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + this + } + + def findRandomUser() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(s => s.set("randomUsername", getRandomUser())) + .exec(http("Console REST - ${realm}/users?first=0&max=20&search=USERNAME") + .get("/auth/admin/realms/${realm}/users?first=0&max=20&search=${randomUsername}") + .headers(AUTHORIZATION) + .check(status.is(200), jsonPath("$[0]['id']").saveAs("userId")) + ) + this + } + + def openUser() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console - user-detail.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/user-detail.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/users/ID") + .get("/auth/admin/realms/${realm}/users/${userId}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console - kc-tabs-user.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-user.html") + .headers(UI_HEADERS) + .check(status.is(200)), + + http("Console REST - ${realm}/authentication/required-actions") + .get("/auth/admin/realms/${realm}/authentication/required-actions") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/attack-detection/brute-force/users/ID") + .get("/auth/admin/realms/${realm}/attack-detection/brute-force/users/${userId}") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + ) + this + } + + def openUserCredentials() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console - user-credentials.html") + .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/user-credentials.html") + .headers(UI_HEADERS) + .check(status.is(200)) + .resources( + + http("Console REST - ${realm}/users/ID") + .get("/auth/admin/realms/${realm}/users/${userId}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}") + .get("/auth/admin/realms/${realm}") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - realms") + .get("/auth/admin/realms") + .headers(AUTHORIZATION) + .check(status.is(200)), + + http("Console REST - ${realm}/authentication/required-actions") + .get("/auth/admin/realms/${realm}/authentication/required-actions") + .headers(AUTHORIZATION) + .check(status.is(200)) + ) + ) + this + } + + def setTemporaryPassword() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Console REST - ${realm}/users/ID/reset-password PUT") + .put("/auth/admin/realms/${realm}/users/${userId}/reset-password") + .headers(AUTHORIZATION) + .header("Content-Type", "application/json") + .body(StringBody("""{"type":"password","value":"testtest","temporary":true}""")) + .check(status.is(204) + ) + ) + this + } + + def logout() : AdminConsoleScenarioBuilder = { + refreshTokenIfExpired() + chainBuilder = chainBuilder + .exec(http("Browser logout") + .get("/auth/realms/master/protocol/openid-connect/logout") + .headers(UI_HEADERS) + .queryParam("redirect_uri", APP_URL) + .check(status.is(302), header("Location").is(APP_URL) + ) + ) + this + } +} diff --git a/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulation.scala b/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulation.scala index d588582e58..836d97c620 100644 --- a/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulation.scala +++ b/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulation.scala @@ -1,12 +1,10 @@ package keycloak import io.gatling.core.Predef._ -import io.gatling.core.validation.Validation import io.gatling.http.Predef._ -import org.jboss.perf.util.Util + +import io.gatling.core.validation.Validation import org.keycloak.performance.TestConfig -import org.keycloak.gatling.Utils._ -import SimulationsHelper._ /** @@ -14,6 +12,12 @@ import SimulationsHelper._ */ class AdminConsoleSimulation extends Simulation { + def rampDownPeriodNotReached(): Validation[Boolean] = { + System.currentTimeMillis < TestConfig.rampDownPeriodStartTime + } + + + println() println("Target server: " + TestConfig.serverUrisList.get(0)) println() @@ -31,91 +35,72 @@ class AdminConsoleSimulation extends Simulation { .acceptLanguageHeader("en-US,en;q=0.5") .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0") - val adminSession = exec(s => { - val realm = TestConfig.randomRealmsIterator().next() - val serverUrl = TestConfig.serverUrisList.get(0) - s.setAll( - "keycloakServer" -> serverUrl, - "keycloakServerUrlEncoded" -> urlencode(serverUrl), - "keycloakServerRootEncoded" -> urlEncodedRoot(serverUrl), - "state" -> Util.randomUUID(), - "nonce" -> Util.randomUUID(), - "randomClientId" -> ("client_" + Util.randomUUID()), - "realm" -> realm, - "username" -> "admin", - "password" -> "admin", - "clientId" -> "security-admin-console" - ) - }) - .exitHereIfFailed + + + val adminSession = new AdminConsoleScenarioBuilder() .openAdminConsoleHome() + .thinkPause() + .loginThroughLoginForm() + + .openRealmSettings() .thinkPause() - .acsim_loginThroughLoginForm() - .exitHereIfFailed + .openClients() .thinkPause() - .acsim_openClients() + .openCreateNewClient() .thinkPause() - .acsim_openCreateNewClient() + .submitNewClient() .thinkPause() - .acsim_submitNewClient() + .updateClient() .thinkPause() - .acsim_updateClient() + .openClients() .thinkPause() - .acsim_openClients() + .openClientDetails() .thinkPause() - .acsim_openClientDetails() + .openUsers() .thinkPause() - .acsim_openUsers() + .viewAllUsers() .thinkPause() - .acsim_viewAllUsers() + .viewTenPagesOfUsers() .thinkPause() - .acsim_viewTenPagesOfUsers() + .find20Users() .thinkPause() - .acsim_find20Users() + .findUnlimitedUsers() .thinkPause() - .acsim_findUnlimitedUsers() + .findRandomUser() + .openUser() .thinkPause() - .acsim_findRandomUser() - - .acsim_openUser() + .openUserCredentials() .thinkPause() - .acsim_openUserCredentials() + .setTemporaryPassword() .thinkPause() - .acsim_setTemporaryPassword() + .logout() .thinkPause() - .acsim_logOut() val adminScenario = scenario("AdminConsole") .asLongAs(s => rampDownPeriodNotReached(), null, TestConfig.rampDownASAP) { pace(TestConfig.pace) - adminSession + adminSession.chainBuilder } setUp(adminScenario .inject(rampUsers(TestConfig.runUsers) over TestConfig.rampUpPeriod) .protocols(httpProtocol)) - - - def rampDownPeriodNotReached(): Validation[Boolean] = { - System.currentTimeMillis < TestConfig.rampDownPeriodStartTime - } - } diff --git a/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulationHelper.scala b/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulationHelper.scala deleted file mode 100644 index af81e38f1d..0000000000 --- a/testsuite/performance/tests/src/test/scala/keycloak/AdminConsoleSimulationHelper.scala +++ /dev/null @@ -1,39 +0,0 @@ -package keycloak - -import java.time.format.DateTimeFormatter - -import io.gatling.core.Predef._ -import org.jboss.perf.util.Util -import org.keycloak.performance.TestConfig - -/** - * @author Marko Strukelj - */ -object AdminConsoleSimulationHelper { - - val UI_HEADERS = Map( - "Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Upgrade-Insecure-Requests" -> "1") - - val ACCEPT_JSON = Map("Accept" -> "application/json") - val ACCEPT_ALL = Map("Accept" -> "*/*") - val AUTHORIZATION = Map("Authorization" -> "Bearer ${accessToken}") - - val APP_URL = "${keycloakServer}/admin/master/console/" - val DATE_FMT = DateTimeFormatter.RFC_1123_DATE_TIME - - - def getRandomUser() : String = { - "user_" + (Util.random.nextDouble() * TestConfig.usersPerRealm).toInt - } - - def needTokenRefresh(sess: Session): Boolean = { - val lastRefresh = sess("accessTokenRefreshTime").as[Long] - - // 5 seconds before expiry is time to refresh - lastRefresh + sess("expiresIn").as[String].toInt * 1000 - 5000 < System.currentTimeMillis() || - // or if refreshTokenPeriod is set force refresh even if not necessary - (TestConfig.refreshTokenPeriod > 0 && - lastRefresh + TestConfig.refreshTokenPeriod * 1000 < System.currentTimeMillis()) - } -} diff --git a/testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCScenarioBuilder.scala b/testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCScenarioBuilder.scala new file mode 100644 index 0000000000..d2df855f2d --- /dev/null +++ b/testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCScenarioBuilder.scala @@ -0,0 +1,175 @@ +package keycloak + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import org.keycloak.gatling.Predef._ +import keycloak.BasicOIDCScenarioBuilder._ + +import java.util.concurrent.atomic.AtomicInteger + +import io.gatling.core.pause.Normal +import io.gatling.core.session.Session +import io.gatling.core.structure.ChainBuilder +import io.gatling.core.validation.Validation +import org.jboss.perf.util.Util +import org.jboss.perf.util.Util.randomUUID +import org.keycloak.adapters.spi.HttpFacade.Cookie +import org.keycloak.gatling.AuthorizeAction +import org.keycloak.performance.TestConfig + + +/** + * @author Marko Strukelj + */ +object BasicOIDCScenarioBuilder { + + val BASE_URL = "${keycloakServer}/realms/${realm}" + val LOGIN_ENDPOINT = BASE_URL + "/protocol/openid-connect/auth" + val LOGOUT_ENDPOINT = BASE_URL + "/protocol/openid-connect/logout" + + // Specify defaults for http requests + val UI_HEADERS = Map( + "Accept" -> "text/html,application/xhtml+xml,application/xml", + "Accept-Encoding" -> "gzip, deflate", + "Accept-Language" -> "en-US,en;q=0.5", + "User-Agent" -> "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0") + + val ACCEPT_JSON = Map("Accept" -> "application/json") + val ACCEPT_ALL = Map("Accept" -> "*/*") + + def downCounterAboveZero(session: Session, attrName: String): Validation[Boolean] = { + val missCounter = session.attributes.get(attrName) match { + case Some(result) => result.asInstanceOf[AtomicInteger] + case None => new AtomicInteger(0) + } + missCounter.getAndDecrement() > 0 + } + + def rampDownPeriodNotReached(): Validation[Boolean] = { + System.currentTimeMillis < TestConfig.rampDownPeriodStartTime + } +} + + +class BasicOIDCScenarioBuilder { + + var chainBuilder = exec(s => { + + // initialize session with host, user, client app, login failure ratio ... + val realm = TestConfig.randomRealmsIterator().next() + val userInfo = TestConfig.getUsersIterator(realm).next() + val clientInfo = TestConfig.getConfidentialClientsIterator(realm).next() + + AuthorizeAction.init(s) + .setAll("keycloakServer" -> TestConfig.serverUrisIterator.next(), + "state" -> randomUUID(), + "wrongPasswordCount" -> new AtomicInteger(TestConfig.badLoginAttempts), + "refreshTokenCount" -> new AtomicInteger(TestConfig.refreshTokenCount), + "realm" -> realm, + "username" -> userInfo.username, + "password" -> userInfo.password, + "clientId" -> clientInfo.clientId, + "secret" -> clientInfo.secret, + "appUrl" -> clientInfo.appUrl + ) + }) + .exitHereIfFailed + + def thinkPause() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder.pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) + this + } + + def thinkPause(builder: ChainBuilder) : ChainBuilder = { + builder.pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) + } + + def newThinkPause() : ChainBuilder = { + pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) + } + + def browserOpensLoginPage() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder + .exec(http("Browser to Log In Endpoint") + .get(LOGIN_ENDPOINT) + .headers(UI_HEADERS) + .queryParam("login", "true") + .queryParam("response_type", "code") + .queryParam("client_id", "${clientId}") + .queryParam("state", "${state}") + .queryParam("redirect_uri", "${appUrl}") + .check(status.is(200), regex("action=\"([^\"]*)\"").find.transform(_.replaceAll("&", "&")).saveAs("login-form-uri"))) + // if already logged in the check will fail with: + // status.find.is(200), but actually found 302 + // The reason is that instead of returning the login page we are immediately redirected to the app that requested authentication + .exitHereIfFailed + this + } + + def browserPostsWrongCredentials() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder + .asLongAs(s => downCounterAboveZero(s, "wrongPasswordCount")) { + var c = exec(http("Browser posts wrong credentials") + .post("${login-form-uri}") + .headers(UI_HEADERS) + .formParam("username", "${username}") + .formParam("password", _ => Util.randomString(10)) + .formParam("login", "Log in") + .check(status.is(200), regex("action=\"([^\"]*)\"").find.transform(_.replaceAll("&", "&")).saveAs("login-form-uri"))) + .exitHereIfFailed + + // make sure to call the right version of thinkPause - one that takes chainBuilder as argument + // - because this is a nested chainBuilder - not the same as chainBuilder field + thinkPause(c) + } + this + } + + def browserPostsCorrectCredentials() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder + .exec(http("Browser posts correct credentials") + .post("${login-form-uri}") + .headers(UI_HEADERS) + .formParam("username", "${username}") + .formParam("password", "${password}") + .formParam("login", "Log in") + .check(status.is(302), header("Location").saveAs("login-redirect"))) + .exitHereIfFailed + this + } + + def adapterExchangesCodeForTokens() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder + .exec(oauth("Adapter exchanges code for tokens") + .authorize("${login-redirect}", + session => List(new Cookie("OAuth_Token_Request_State", session("state").as[String], 0, null, null))) + .authServerUrl("${keycloakServer}") + .resource("${clientId}") + .clientCredentials("${secret}") + .realm("${realm}") + //.realmKey(Loader.realmRepresentation.getPublicKey) + ) + this + } + + def refreshTokenSeveralTimes() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder + .asLongAs(s => downCounterAboveZero(s, "refreshTokenCount")) { + // make sure to call newThinkPause rather than thinkPause + newThinkPause() + .exec(oauth("Adapter refreshes token").refresh()) + } + this + } + + def logout() : BasicOIDCScenarioBuilder = { + chainBuilder = chainBuilder + .exec(http("Browser logout") + .get(LOGOUT_ENDPOINT) + .headers(UI_HEADERS) + .queryParam("redirect_uri", "${appUrl}") + .check(status.is(302), header("Location").is("${appUrl}"))) + this + } +} + diff --git a/testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCSimulation.scala b/testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCSimulation.scala new file mode 100644 index 0000000000..131de09b36 --- /dev/null +++ b/testsuite/performance/tests/src/test/scala/keycloak/BasicOIDCSimulation.scala @@ -0,0 +1,61 @@ +package keycloak + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import keycloak.BasicOIDCScenarioBuilder._ + +import org.keycloak.performance.TestConfig + + +/** + * @author Radim Vansa <rvansa@redhat.com> + * @author Marko Strukelj <mstrukel@redhat.com> + */ +class BasicOIDCSimulation extends Simulation { + + println() + println("Target servers: " + TestConfig.serverUrisList) + println() + + println("Using test parameters:\n" + TestConfig.toStringCommonTestParameters); + println(" refreshTokenCount: " + TestConfig.refreshTokenCount) + println(" badLoginAttempts: " + TestConfig.badLoginAttempts) + println() + println("Using dataset properties:\n" + TestConfig.toStringDatasetProperties) + + + val httpDefault = http + .acceptHeader("application/json") + .disableFollowRedirect + .inferHtmlResources + + val userSession = new BasicOIDCScenarioBuilder() + + .browserOpensLoginPage() + + .thinkPause() + .browserPostsWrongCredentials() + .browserPostsCorrectCredentials() + + // Act as client adapter - exchange code for keys + .adapterExchangesCodeForTokens() + + .refreshTokenSeveralTimes() + + .thinkPause() + .logout() + + .thinkPause() + + + val usersScenario = scenario("users") + .asLongAs(s => rampDownPeriodNotReached(), null, TestConfig.rampDownASAP) { + pace(TestConfig.pace) + userSession.chainBuilder + } + + setUp(usersScenario + .inject(rampUsers(TestConfig.runUsers) over TestConfig.rampUpPeriod) + .protocols(httpDefault)) + +} diff --git a/testsuite/performance/tests/src/test/scala/keycloak/DefaultSimulation.scala b/testsuite/performance/tests/src/test/scala/keycloak/DefaultSimulation.scala deleted file mode 100644 index 3f0d6e7ba0..0000000000 --- a/testsuite/performance/tests/src/test/scala/keycloak/DefaultSimulation.scala +++ /dev/null @@ -1,161 +0,0 @@ -package keycloak - -import java.util.concurrent.atomic.AtomicInteger - -import io.gatling.core.Predef._ -import io.gatling.core.pause.Normal -import io.gatling.core.session._ -import io.gatling.core.validation.Validation -import io.gatling.http.Predef._ -import org.jboss.perf.util.Util -import org.keycloak.adapters.spi.HttpFacade.Cookie -import org.keycloak.gatling.AuthorizeAction -import org.keycloak.gatling.Predef._ -import org.keycloak.performance.TestConfig - -/** - * @author Radim Vansa <rvansa@redhat.com> - * @author Marko Strukelj <mstrukel@redhat.com> - */ -class DefaultSimulation extends Simulation { - - val BASE_URL = "${keycloakServer}/realms/${realm}" - val LOGIN_ENDPOINT = BASE_URL + "/protocol/openid-connect/auth" - val LOGOUT_ENDPOINT = BASE_URL + "/protocol/openid-connect/logout" - - - - println() - println("Target servers: " + TestConfig.serverUrisList) - println() - - println("Using test parameters:\n" + TestConfig.toStringCommonTestParameters); - println(" refreshTokenCount: " + TestConfig.refreshTokenCount) - println(" badLoginAttempts: " + TestConfig.badLoginAttempts) - println() - println("Using dataset properties:\n" + TestConfig.toStringDatasetProperties) - - - val httpDefault = http - .acceptHeader("application/json") - .disableFollowRedirect - .inferHtmlResources - //.baseURL(SERVER_URI) - - // Specify defaults for http requests - val UI_HEADERS = Map( - "Accept" -> "text/html,application/xhtml+xml,application/xml", - "Accept-Encoding" -> "gzip, deflate", - "Accept-Language" -> "en-US,en;q=0.5", - "User-Agent" -> "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0") - - val ACCEPT_JSON = Map("Accept" -> "application/json") - val ACCEPT_ALL = Map("Accept" -> "*/*") - - val userSession = exec(s => { - // initialize session with host, user, client app, login failure ratio ... - val realm = TestConfig.randomRealmsIterator().next() - val userInfo = TestConfig.getUsersIterator(realm).next() - val clientInfo = TestConfig.getConfidentialClientsIterator(realm).next() - - AuthorizeAction.init(s) - .setAll("keycloakServer" -> TestConfig.serverUrisIterator.next(), - "state" -> Util.randomUUID(), - "wrongPasswordCount" -> new AtomicInteger(TestConfig.badLoginAttempts), - "refreshTokenCount" -> new AtomicInteger(TestConfig.refreshTokenCount), - "realm" -> realm, - "username" -> userInfo.username, - "password" -> userInfo.password, - "clientId" -> clientInfo.clientId, - "secret" -> clientInfo.secret, - "appUrl" -> clientInfo.appUrl - ) - }) - .exitHereIfFailed - .exec(http("Browser to Log In Endpoint") - .get(LOGIN_ENDPOINT) - .headers(UI_HEADERS) - .queryParam("login", "true") - .queryParam("response_type", "code") - .queryParam("client_id", "${clientId}") - .queryParam("state", "${state}") - .queryParam("redirect_uri", "${appUrl}") - .check(status.is(200), regex("action=\"([^\"]*)\"").find.transform(_.replaceAll("&", "&")).saveAs("login-form-uri"))) - .exitHereIfFailed - .pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) - - .asLongAs(s => downCounterAboveZero(s, "wrongPasswordCount")) { - exec(http("Browser posts wrong credentials") - .post("${login-form-uri}") - .headers(UI_HEADERS) - .formParam("username", "${username}") - .formParam("password", _ => Util.randomString(10)) - .formParam("login", "Log in") - .check(status.is(200), regex("action=\"([^\"]*)\"").find.transform(_.replaceAll("&", "&")).saveAs("login-form-uri"))) - .exitHereIfFailed - .pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) - } - - // Successful login - .exec(http("Browser posts correct credentials") - .post("${login-form-uri}") - .headers(UI_HEADERS) - .formParam("username", "${username}") - .formParam("password", "${password}") - .formParam("login", "Log in") - .check(status.is(302), header("Location").saveAs("login-redirect"))) - .exitHereIfFailed - - - // Now act as client adapter - exchange code for keys - .exec(oauth("Adapter exchanges code for tokens") - .authorize("${login-redirect}", - session => List(new Cookie("OAuth_Token_Request_State", session("state").as[String], 0, null, null))) - .authServerUrl("${keycloakServer}") - .resource("${clientId}") - .clientCredentials("${secret}") - .realm("${realm}") - //.realmKey(Loader.realmRepresentation.getPublicKey) - ) - - // Refresh token several times - .asLongAs(s => downCounterAboveZero(s, "refreshTokenCount")) { - pause(TestConfig.refreshTokenPeriod, Normal(TestConfig.refreshTokenPeriod * 0.2)) - .exec(oauth("Adapter refreshes token").refresh()) - } - - // Logout - .pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) - .exec(http("Browser logout") - .get(LOGOUT_ENDPOINT) - .headers(UI_HEADERS) - .queryParam("redirect_uri", "${appUrl}") - .check(status.is(302), header("Location").is("${appUrl}"))) - - val usersScenario = scenario("users") - .asLongAs(s => rampDownPeriodNotReached(), null, TestConfig.rampDownASAP) { - pace(TestConfig.pace) - userSession - } - - setUp(usersScenario - .inject(rampUsers(TestConfig.runUsers) over TestConfig.rampUpPeriod) - .protocols(httpDefault)) - - // - // Function definitions - // - - def downCounterAboveZero(session: Session, attrName: String): Validation[Boolean] = { - val missCounter = session.attributes.get(attrName) match { - case Some(result) => result.asInstanceOf[AtomicInteger] - case None => new AtomicInteger(0) - } - missCounter.getAndDecrement() > 0 - } - - def rampDownPeriodNotReached(): Validation[Boolean] = { - System.currentTimeMillis < TestConfig.rampDownPeriodStartTime - } - -} diff --git a/testsuite/performance/tests/src/test/scala/keycloak/SimulationsHelper.scala b/testsuite/performance/tests/src/test/scala/keycloak/SimulationsHelper.scala deleted file mode 100644 index fdcbad4db8..0000000000 --- a/testsuite/performance/tests/src/test/scala/keycloak/SimulationsHelper.scala +++ /dev/null @@ -1,557 +0,0 @@ -package keycloak - -import java.time.ZonedDateTime - -import io.gatling.core.Predef._ -import io.gatling.http.Predef._ - -import io.gatling.core.pause.Normal -import io.gatling.core.structure.ChainBuilder -import keycloak.AdminConsoleSimulationHelper._ - -import org.keycloak.performance.TestConfig - - -/** - * @author Marko Strukelj - */ -object SimulationsHelper { - - implicit class SimulationsChainBuilderExtras(val builder: ChainBuilder) { - - def acsim_refreshTokenIfExpired() : ChainBuilder = { - builder - .doIf(s => needTokenRefresh(s)) { - exec(http("JS Adapter Token - Refresh tokens") - .post("/auth/realms/master/protocol/openid-connect/token") - .headers(ACCEPT_ALL) - .formParam("grant_type", "refresh_token") - .formParam("refresh_token", "${refreshToken}") - .formParam("client_id", "security-admin-console") - .check(status.is(200), - jsonPath("$.access_token").saveAs("accessToken"), - jsonPath("$.refresh_token").saveAs("refreshToken"), - jsonPath("$.expires_in").saveAs("expiresIn"), - header("Date").saveAs("tokenTime"))) - - .exec(s => { - s.set("accessTokenRefreshTime", ZonedDateTime.parse(s("tokenTime").as[String], DATE_FMT).toEpochSecond * 1000) - }) - } - } - - def openAdminConsoleHome() : ChainBuilder = { - builder - .exec(http("Console Home") - .get("/auth/admin/") - .headers(UI_HEADERS) - .check(status.is(302)) - .resources( - http("Console Redirect") - .get("/auth/admin/master/console/") - .headers(UI_HEADERS) - .check(status.is(200), regex("").saveAs("resourceVersion")), - http("Console REST - Config") - .get("/auth/admin/master/console/config") - .headers(ACCEPT_JSON) - .check(status.is(200)) - ) - ) - } - - def acsim_loginThroughLoginForm() : ChainBuilder = { - builder - .exec(http("JS Adapter Auth - Login Form Redirect") - .get("/auth/realms/master/protocol/openid-connect/auth?client_id=security-admin-console&redirect_uri=${keycloakServerUrlEncoded}%2Fadmin%2Fmaster%2Fconsole%2F&state=${state}&nonce=${nonce}&response_mode=fragment&response_type=code&scope=openid") - .headers(UI_HEADERS) - .check(status.is(200), regex("action=\"([^\"]*)\"").find.transform(_.replaceAll("&", "&")).saveAs("login-form-uri"))) - .exitHereIfFailed - .thinkPause() - // Successful login - .exec(http("Login Form - Submit Correct Credentials") - .post("${login-form-uri}") - .formParam("username", "${username}") - .formParam("password", "${password}") - .formParam("login", "Log in") - .check(status.is(302), - header("Location").saveAs("login-redirect"), - headerRegex("Location", "code=([^&]+)").saveAs("code"))) - // TODO store AUTH_SESSION_ID cookie for use with oauth.authorize? - .exitHereIfFailed - .exec(http("Console Redirect") - .get("/auth/admin/master/console/") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - http("Console REST - Config") - .get("/auth/admin/master/console/config") - .headers(ACCEPT_JSON) - .check(status.is(200)), - - http("JS Adapter Token - Exchange code for tokens") - .post("/auth/realms/master/protocol/openid-connect/token") - .headers(ACCEPT_ALL) - .formParam("code", "${code}") - .formParam("grant_type", "authorization_code") - .formParam("client_id", "security-admin-console") - .formParam("redirect_uri", APP_URL) - .check(status.is(200), - jsonPath("$.access_token").saveAs("accessToken"), - jsonPath("$.refresh_token").saveAs("refreshToken"), - jsonPath("$.expires_in").saveAs("expiresIn"), - header("Date").saveAs("tokenTime")), - - http("Console REST - messages.json") - .get("/auth/admin/master/console/messages.json?lang=en") - .headers(ACCEPT_JSON) - .check(status.is(200)), - - // iframe status listener - // TODO: properly set Referer - http("IFrame Status Init") - .get("/auth/realms/master/protocol/openid-connect/login-status-iframe.html/init?client_id=security-admin-console&origin=${keycloakServerRootEncoded}") // ${keycloakServerUrlEncoded} - .headers(ACCEPT_ALL) // ++ Map("Referer" -> "/auth/realms/master/protocol/openid-connect/login-status-iframe.html?version=3.3.0.cr1-201708011508") ${resourceVersion} - .check(status.is(204)) - ) - ) - .exec(s => { - // How to not have to duplicate this block of code? - s.set("accessTokenRefreshTime", ZonedDateTime.parse(s("tokenTime").as[String], DATE_FMT).toEpochSecond * 1000) - }) - .exec(http("Console REST - whoami") - .get("/auth/admin/master/console/whoami") - .headers(ACCEPT_JSON ++ AUTHORIZATION) - .check(status.is(200))) - - .exec(http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200))) - - .exec(http("Console REST - serverinfo") - .get("/auth/admin/serverinfo") - .headers(AUTHORIZATION) - .check(status.is(200))) - - .exec(http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200))) - - .exec(http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200))) - - .exec(http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200))) - - // DO NOT forget the leading dot, or the wrong ScenarioBuilder will be returned - .acsim_openRealmSettings() - } - - def acsim_openRealmSettings() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console Realm Settings") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/realm-detail.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console - kc-tabs-realm.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-realm.html") - //.headers(UI_HEADERS ++ Map("Referer" -> "")) // TODO fix referer - .headers(UI_HEADERS) - .check(status.is(200)), - - http("Console - kc-menu.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-menu.html") - //.headers(UI_HEADERS ++ Map("Referer" -> "")) // TODO fix referer - .headers(UI_HEADERS) - .check(status.is(200)), - - // request fonts for css also set referer - http("OpenSans-Semibold-webfont.woff") - .get("/auth/resources/${resourceVersion}/admin/keycloak/lib/patternfly/fonts/OpenSans-Semibold-webfont.woff") - .headers(UI_HEADERS) - .check(status.is(200)), - - http("OpenSans-Bold-webfont.woff") - .get("/auth/resources/${resourceVersion}/admin/keycloak/lib/patternfly/fonts/OpenSans-Bold-webfont.woff") - .headers(UI_HEADERS) - .check(status.is(200)), - - http("OpenSans-Light-webfont.woff") - .get("/auth/resources/${resourceVersion}/admin/keycloak/lib/patternfly/fonts/OpenSans-Light-webfont.woff") - .headers(UI_HEADERS) - .check(status.is(200)) - ) - ) - } - - def acsim_openClients() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console - client-list.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/client-list.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - http("Console - kc-paging.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-paging.html") - .headers(UI_HEADERS) - .check(status.is(200)), - http("Console REST - ${realm}/clients") - .get("/auth/admin/realms/${realm}/clients?viewableOnly=true") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - ) - } - - def acsim_openCreateNewClient() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console - create-client.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/create-client.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - http("Console REST - ${realm}/clients") - .get("/auth/admin/realms/${realm}/clients") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - ) - } - - def acsim_submitNewClient() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console REST - ${realm}/clients POST") - .post("/auth/admin/realms/${realm}/clients") - .headers(AUTHORIZATION) - .header("Content-Type", "application/json") - .body(StringBody(""" - {"enabled":true,"attributes":{},"redirectUris":[],"clientId":"${randomClientId}","rootUrl":"http://localhost:8081/myapp","protocol":"openid-connect"} - """.stripMargin)) - .check(status.is(201), headerRegex("Location", "\\/([^\\/]+)$").saveAs("idOfClient"))) - - .exec(http("Console REST - ${realm}/clients/ID") - .get("/auth/admin/realms/${realm}/clients/${idOfClient}") - .headers(AUTHORIZATION) - .check(status.is(200), bodyString.saveAs("clientJson")) - .resources( - http("Console REST - ${realm}/clients") - .get("/auth/admin/realms/${realm}/clients") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/client-templates") - .get("/auth/admin/realms/${realm}/client-templates") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - ) - } - - def acsim_updateClient() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(s => { - s.set("updateClientJson", s("clientJson").as[String].replace("\"publicClient\":false", "\"publicClient\":true")) - }) - .exec(http("Console REST - ${realm}/clients/ID PUT") - .put("/auth/admin/realms/${realm}/clients/${idOfClient}") - .headers(AUTHORIZATION) - .header("Content-Type", "application/json") - .body(StringBody("${updateClientJson}")) - .check(status.is(204))) - - .exec(http("Console REST - ${realm}/clients/ID") - .get("/auth/admin/realms/${realm}/clients/${idOfClient}") - .headers(AUTHORIZATION) - .check(status.is(200), bodyString.saveAs("clientJson")) - .resources( - http("Console REST - ${realm}/clients") - .get("/auth/admin/realms/${realm}/clients") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/client-templates") - .get("/auth/admin/realms/${realm}/client-templates") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - ) - } - - def acsim_openClientDetails() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console - client-detail.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/client-detail.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - http("Console REST - ${realm}/client-templates") - .get("/auth/admin/realms/${realm}/client-templates") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/clients") - .get("/auth/admin/realms/${realm}/clients") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/clients/ID") - .get("/auth/admin/realms/${realm}/clients/${idOfClient}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console - kc-tabs-client.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-client.html") - .headers(UI_HEADERS) - .check(status.is(200)) - ) - ) - } - - def acsim_openUsers() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console - user-list.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/user-list.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - http("Console - kc-tabs-users.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-users.html") - .headers(UI_HEADERS) - .check(status.is(200)) - ) - ) - } - - def acsim_viewAllUsers() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console REST - ${realm}/users") - .get("/auth/admin/realms/${realm}/users?first=0&max=20") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - } - - def acsim_viewTenPagesOfUsers() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .repeat(10, "i") { - exec(s => s.set("offset", s("i").as[Int]*20)) - .pause(1) - .exec(http("Console REST - ${realm}/users?first=${offset}") - .get("/auth/admin/realms/${realm}/users?first=${offset}&max=20") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - } - } - - def acsim_find20Users() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console REST - ${realm}/users?first=0&max=20&search=user") - .get("/auth/admin/realms/${realm}/users?first=0&max=20&search=user") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - } - - def acsim_findUnlimitedUsers() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console REST - ${realm}/users?search=user") - .get("/auth/admin/realms/${realm}/users?search=user") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - } - - def acsim_findRandomUser() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(s => s.set("randomUsername", AdminConsoleSimulationHelper.getRandomUser())) - .exec(http("Console REST - ${realm}/users?first=0&max=20&search=USERNAME") - .get("/auth/admin/realms/${realm}/users?first=0&max=20&search=${randomUsername}") - .headers(AUTHORIZATION) - .check(status.is(200), jsonPath("$[0]['id']").saveAs("userId")) - ) - } - - def acsim_openUser() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console - user-detail.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/user-detail.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/users/ID") - .get("/auth/admin/realms/${realm}/users/${userId}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console - kc-tabs-user.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/templates/kc-tabs-user.html") - .headers(UI_HEADERS) - .check(status.is(200)), - - http("Console REST - ${realm}/authentication/required-actions") - .get("/auth/admin/realms/${realm}/authentication/required-actions") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/attack-detection/brute-force/users/ID") - .get("/auth/admin/realms/${realm}/attack-detection/brute-force/users/${userId}") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - ) - } - - def acsim_openUserCredentials() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console - user-credentials.html") - .get("/auth/resources/${resourceVersion}/admin/keycloak/partials/user-credentials.html") - .headers(UI_HEADERS) - .check(status.is(200)) - .resources( - - http("Console REST - ${realm}/users/ID") - .get("/auth/admin/realms/${realm}/users/${userId}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}") - .get("/auth/admin/realms/${realm}") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - realms") - .get("/auth/admin/realms") - .headers(AUTHORIZATION) - .check(status.is(200)), - - http("Console REST - ${realm}/authentication/required-actions") - .get("/auth/admin/realms/${realm}/authentication/required-actions") - .headers(AUTHORIZATION) - .check(status.is(200)) - ) - ) - } - - def acsim_setTemporaryPassword() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Console REST - ${realm}/users/ID/reset-password PUT") - .put("/auth/admin/realms/${realm}/users/${userId}/reset-password") - .headers(AUTHORIZATION) - .header("Content-Type", "application/json") - .body(StringBody("""{"type":"password","value":"testtest","temporary":true}""")) - .check(status.is(204) - ) - ) - } - - def acsim_logOut() : ChainBuilder = { - builder - .acsim_refreshTokenIfExpired() - .exec(http("Browser logout") - .get("/auth/realms/master/protocol/openid-connect/logout") - .headers(UI_HEADERS) - .queryParam("redirect_uri", APP_URL) - .check(status.is(302), header("Location").is(APP_URL) - ) - ) - } - - def thinkPause() : ChainBuilder = { - builder.pause(TestConfig.userThinkTime, Normal(TestConfig.userThinkTime * 0.2)) - } - } -}