KEYCLOAK-6514 Common approach to writing performance tests

This commit is contained in:
Marko Strukelj 2018-02-20 17:17:13 +01:00 committed by Pavel Drozd
parent b464dc15f2
commit 548ab4f78c
13 changed files with 924 additions and 817 deletions

View file

@ -7,7 +7,7 @@ Perform the usual test run:
mvn verify -Pteardown mvn verify -Pteardown
mvn verify -Pprovision mvn verify -Pprovision
mvn verify -Pgenerate-data -Ddataset=100users -Dimport.workers=10 -DhashIterations=100 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): Now analyze the generated simulation.log (adjust LOG_DIR, FROM, and TO):

View file

@ -27,7 +27,7 @@ mvn clean install
# Make sure your Docker daemon is running THEN # Make sure your Docker daemon is running THEN
mvn verify -Pprovision mvn verify -Pprovision
mvn verify -Pgenerate-data -Ddataset=100u -DnumOfWorkers=10 -DhashIterations=100 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 | | 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` | | `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` | | `runUsers` | Number of users for the simulation run. | `1` |
| `rampUpPeriod` | Period during which the users will be ramped up. (seconds) | `0` | | `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` | | `userThinkTime` | Pause between individual scenario steps. | `5` |
| `refreshTokenPeriod`| Period after which token should be refreshed. | `10` | | `refreshTokenPeriod`| Period after which token should be refreshed. | `10` |
#### Addtional Parameters of `keycloak.DefaultSimulation` #### Addtional Parameters of `keycloak.BasicOIDCSimulation`
| Parameter | Description | Default Value | | Parameter | Description | Default Value |
| --- | --- | --- | | --- | --- | --- |
@ -159,7 +159,7 @@ Usage: `mvn verify -Ptest[,cluster] [-DtestParameter=value]`.
Example: 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 ## 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'. 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. 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 Make sure that `performance` maven profile is enabled for IDEA to treat `performance` directory as a project module.
JUnit tests.
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'. Make sure to set 'Use classpath of module' to 'performance-test'.

View file

@ -93,7 +93,7 @@
<scala-maven-plugin.version>3.2.2</scala-maven-plugin.version> <scala-maven-plugin.version>3.2.2</scala-maven-plugin.version>
<jboss-logging.version>3.3.0.Final</jboss-logging.version> <jboss-logging.version>3.3.0.Final</jboss-logging.version>
<gatling.simulationClass>keycloak.DefaultSimulation</gatling.simulationClass> <gatling.simulationClass>keycloak.BasicOIDCSimulation</gatling.simulationClass>
<gatling.skip.run>true</gatling.skip.run> <gatling.skip.run>true</gatling.skip.run>
</properties> </properties>

View file

@ -68,7 +68,7 @@ public class TestConfig {
public static final long rampDownPeriodStartTime = simulationStartTime + (rampUpPeriod + steadyLoadPeriod) * 1000; 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 badLoginAttempts = Integer.getInteger("badLoginAttempts", 0);
public static final int refreshTokenCount = Integer.getInteger("refreshTokenCount", 0); public static final int refreshTokenCount = Integer.getInteger("refreshTokenCount", 0);

View file

@ -4,8 +4,8 @@ import io.gatling.core.config.GatlingPropertiesBuilder
object Engine extends App { object Engine extends App {
val sim = classOf[keycloak.DefaultSimulation] val sim = classOf[keycloak.BasicOIDCSimulation]
//val sim = classOf[keycloak.AdminSimulation] //val sim = classOf[keycloak.AdminConsoleSimulation]
val props = new GatlingPropertiesBuilder val props = new GatlingPropertiesBuilder
props.dataDirectory(IDEPathHelper.dataDirectory.toString) props.dataDirectory(IDEPathHelper.dataDirectory.toString)

View file

@ -24,6 +24,7 @@ class SimpleExample4 extends Simulation {
.exec(account) .exec(account)
setUp( 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) scn.inject(rampUsers(100) over 10).protocols(httpConf)
) )
} }

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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("<link.+\\/resources\\/([^\\/]+).+>").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("&amp;", "&")).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
}
}

View file

@ -1,12 +1,10 @@
package keycloak package keycloak
import io.gatling.core.Predef._ import io.gatling.core.Predef._
import io.gatling.core.validation.Validation
import io.gatling.http.Predef._ 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.performance.TestConfig
import org.keycloak.gatling.Utils._
import SimulationsHelper._
/** /**
@ -14,6 +12,12 @@ import SimulationsHelper._
*/ */
class AdminConsoleSimulation extends Simulation { class AdminConsoleSimulation extends Simulation {
def rampDownPeriodNotReached(): Validation[Boolean] = {
System.currentTimeMillis < TestConfig.rampDownPeriodStartTime
}
println() println()
println("Target server: " + TestConfig.serverUrisList.get(0)) println("Target server: " + TestConfig.serverUrisList.get(0))
println() println()
@ -31,91 +35,72 @@ class AdminConsoleSimulation extends Simulation {
.acceptLanguageHeader("en-US,en;q=0.5") .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") .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() .openAdminConsoleHome()
.thinkPause()
.loginThroughLoginForm()
.openRealmSettings()
.thinkPause() .thinkPause()
.acsim_loginThroughLoginForm() .openClients()
.exitHereIfFailed
.thinkPause() .thinkPause()
.acsim_openClients() .openCreateNewClient()
.thinkPause() .thinkPause()
.acsim_openCreateNewClient() .submitNewClient()
.thinkPause() .thinkPause()
.acsim_submitNewClient() .updateClient()
.thinkPause() .thinkPause()
.acsim_updateClient() .openClients()
.thinkPause() .thinkPause()
.acsim_openClients() .openClientDetails()
.thinkPause() .thinkPause()
.acsim_openClientDetails() .openUsers()
.thinkPause() .thinkPause()
.acsim_openUsers() .viewAllUsers()
.thinkPause() .thinkPause()
.acsim_viewAllUsers() .viewTenPagesOfUsers()
.thinkPause() .thinkPause()
.acsim_viewTenPagesOfUsers() .find20Users()
.thinkPause() .thinkPause()
.acsim_find20Users() .findUnlimitedUsers()
.thinkPause() .thinkPause()
.acsim_findUnlimitedUsers() .findRandomUser()
.openUser()
.thinkPause() .thinkPause()
.acsim_findRandomUser() .openUserCredentials()
.acsim_openUser()
.thinkPause() .thinkPause()
.acsim_openUserCredentials() .setTemporaryPassword()
.thinkPause() .thinkPause()
.acsim_setTemporaryPassword() .logout()
.thinkPause() .thinkPause()
.acsim_logOut()
val adminScenario = scenario("AdminConsole") val adminScenario = scenario("AdminConsole")
.asLongAs(s => rampDownPeriodNotReached(), null, TestConfig.rampDownASAP) { .asLongAs(s => rampDownPeriodNotReached(), null, TestConfig.rampDownASAP) {
pace(TestConfig.pace) pace(TestConfig.pace)
adminSession adminSession.chainBuilder
} }
setUp(adminScenario setUp(adminScenario
.inject(rampUsers(TestConfig.runUsers) over TestConfig.rampUpPeriod) .inject(rampUsers(TestConfig.runUsers) over TestConfig.rampUpPeriod)
.protocols(httpProtocol)) .protocols(httpProtocol))
def rampDownPeriodNotReached(): Validation[Boolean] = {
System.currentTimeMillis < TestConfig.rampDownPeriodStartTime
}
} }

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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())
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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("&amp;", "&")).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("&amp;", "&")).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
}
}

View file

@ -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 &lt;rvansa@redhat.com&gt;
* @author Marko Strukelj &lt;mstrukel@redhat.com&gt;
*/
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))
}

View file

@ -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 &lt;rvansa@redhat.com&gt;
* @author Marko Strukelj &lt;mstrukel@redhat.com&gt;
*/
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("&amp;", "&")).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("&amp;", "&")).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
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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("<link.+\\/resources\\/([^\\/]+).+>").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("&amp;", "&")).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))
}
}
}