KEYCLOAK-4827 Add tests for concurrent use of user session in cache
This commit is contained in:
parent
f392e79ad7
commit
cc6a5419de
3 changed files with 346 additions and 80 deletions
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.admin.concurrency;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.Keycloak;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractConcurrencyTest extends AbstractAdminTest {
|
||||||
|
|
||||||
|
private static final int DEFAULT_THREADS = 5;
|
||||||
|
private static final int DEFAULT_ITERATIONS = 20;
|
||||||
|
|
||||||
|
// If enabled only one request is allowed at the time. Useful for checking that test is working.
|
||||||
|
private static final boolean SYNCHRONIZED = false;
|
||||||
|
|
||||||
|
protected void run(final KeycloakRunnable runnable) throws Throwable {
|
||||||
|
run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable {
|
||||||
|
final CountDownLatch latch = new CountDownLatch(numThreads);
|
||||||
|
final AtomicReference<Throwable> failed = new AtomicReference();
|
||||||
|
final List<Thread> threads = new LinkedList<>();
|
||||||
|
final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null;
|
||||||
|
|
||||||
|
for (int t = 0; t < numThreads; t++) {
|
||||||
|
final int threadNum = t;
|
||||||
|
Thread thread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Keycloak keycloak = null;
|
||||||
|
try {
|
||||||
|
if (lock != null) {
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
|
||||||
|
RealmResource realm = keycloak.realm(REALM_NAME);
|
||||||
|
for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) {
|
||||||
|
log.infov("thread {0}, iteration {1}", threadNum, i);
|
||||||
|
runnable.run(keycloak, realm, threadNum, i);
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failed.compareAndSet(null, t);
|
||||||
|
while (latch.getCount() > 0) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
keycloak.close();
|
||||||
|
if (lock != null) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.start();
|
||||||
|
threads.add(thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await();
|
||||||
|
|
||||||
|
for (Thread t : threads) {
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed.get() != null) {
|
||||||
|
throw failed.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected interface KeycloakRunnable {
|
||||||
|
|
||||||
|
void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,9 +15,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.admin;
|
package org.keycloak.testsuite.admin.concurrency;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -30,12 +29,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.LinkedList;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
@ -43,15 +37,7 @@ import static org.junit.Assert.fail;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class ConcurrencyTest extends AbstractAdminTest {
|
public class ConcurrencyTest extends AbstractConcurrencyTest {
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(ConcurrencyTest.class);
|
|
||||||
|
|
||||||
private static final int DEFAULT_THREADS = 5;
|
|
||||||
private static final int DEFAULT_ITERATIONS = 20;
|
|
||||||
|
|
||||||
// If enabled only one request is allowed at the time. Useful for checking that test is working.
|
|
||||||
private static final boolean SYNCHRONIZED = false;
|
|
||||||
|
|
||||||
boolean passedCreateClient = false;
|
boolean passedCreateClient = false;
|
||||||
boolean passedCreateRole = false;
|
boolean passedCreateRole = false;
|
||||||
|
@ -252,67 +238,4 @@ public class ConcurrencyTest extends AbstractAdminTest {
|
||||||
System.out.println("*********************************************");
|
System.out.println("*********************************************");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void run(final KeycloakRunnable runnable) throws Throwable {
|
|
||||||
run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void run(final KeycloakRunnable runnable, final int numThreads, final int numIterationsPerThread) throws Throwable {
|
|
||||||
final CountDownLatch latch = new CountDownLatch(numThreads);
|
|
||||||
final AtomicReference<Throwable> failed = new AtomicReference();
|
|
||||||
final List<Thread> threads = new LinkedList<>();
|
|
||||||
final Lock lock = SYNCHRONIZED ? new ReentrantLock() : null;
|
|
||||||
|
|
||||||
for (int t = 0; t < numThreads; t++) {
|
|
||||||
final int threadNum = t;
|
|
||||||
Thread thread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Keycloak keycloak = null;
|
|
||||||
try {
|
|
||||||
if (lock != null) {
|
|
||||||
lock.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
keycloak = Keycloak.getInstance(getAuthServerRoot().toString(), "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID);
|
|
||||||
RealmResource realm = keycloak.realm(REALM_NAME);
|
|
||||||
for (int i = 0; i < numIterationsPerThread && latch.getCount() > 0; i++) {
|
|
||||||
log.infov("thread {0}, iteration {1}", threadNum, i);
|
|
||||||
runnable.run(keycloak, realm, threadNum, i);
|
|
||||||
}
|
|
||||||
latch.countDown();
|
|
||||||
} catch (Throwable t) {
|
|
||||||
failed.compareAndSet(null, t);
|
|
||||||
while (latch.getCount() > 0) {
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
keycloak.close();
|
|
||||||
if (lock != null) {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
thread.start();
|
|
||||||
threads.add(thread);
|
|
||||||
}
|
|
||||||
|
|
||||||
latch.await();
|
|
||||||
|
|
||||||
for (Thread t : threads) {
|
|
||||||
t.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed.get() != null) {
|
|
||||||
throw failed.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface KeycloakRunnable {
|
|
||||||
|
|
||||||
void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.admin.concurrency;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.apache.http.NameValuePair;
|
||||||
|
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.client.methods.HttpUriRequest;
|
||||||
|
import org.apache.http.client.protocol.HttpClientContext;
|
||||||
|
import org.apache.http.client.utils.URLEncodedUtils;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.apache.http.impl.client.LaxRedirectStrategy;
|
||||||
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.admin.client.Keycloak;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
|
||||||
|
*/
|
||||||
|
public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
||||||
|
|
||||||
|
private static final int DEFAULT_THREADS = 10;
|
||||||
|
private static final int DEFAULT_ITERATIONS = 20;
|
||||||
|
private static final int CLIENTS_PER_THREAD = 10;
|
||||||
|
private static final int DEFAULT_CLIENTS_COUNT = CLIENTS_PER_THREAD * DEFAULT_THREADS;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() {
|
||||||
|
for (int i = 0; i < DEFAULT_CLIENTS_COUNT; i++) {
|
||||||
|
ClientRepresentation client = new ClientRepresentation();
|
||||||
|
client.setClientId("client" + i);
|
||||||
|
client.setDirectAccessGrantsEnabled(true);
|
||||||
|
client.setRedirectUris(Arrays.asList("http://localhost:8180/auth/realms/master/app/*"));
|
||||||
|
client.setWebOrigins(Arrays.asList("http://localhost:8180"));
|
||||||
|
client.setSecret("password");
|
||||||
|
|
||||||
|
log.debug("creating " + client.getClientId());
|
||||||
|
Response create = adminClient.realm("test").clients().create(client);
|
||||||
|
Assert.assertEquals(Response.Status.CREATED, create.getStatusInfo());
|
||||||
|
create.close();
|
||||||
|
}
|
||||||
|
log.debug("clients created");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void run(final KeycloakRunnable runnable) throws Throwable {
|
||||||
|
run(runnable, DEFAULT_THREADS, DEFAULT_ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentLogin() throws Throwable {
|
||||||
|
System.out.println("*********************************************");
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build()) {
|
||||||
|
|
||||||
|
HttpUriRequest request = handleLogin(getPageContent(oauth.getLoginFormUrl(), httpClient, null), "test-user@localhost", "password");
|
||||||
|
|
||||||
|
log.debug("Executing login request");
|
||||||
|
|
||||||
|
Assert.assertTrue(parseAndCloseResponse(httpClient.execute(request)).contains("<title>AUTH_RESPONSE</title>"));
|
||||||
|
|
||||||
|
run(new KeycloakRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run(Keycloak keycloak, RealmResource realm, int threadNum, int iterationNum) {
|
||||||
|
OAuthClient oauth = new OAuthClient();
|
||||||
|
oauth.init(adminClient, driver);
|
||||||
|
|
||||||
|
int startIndex = CLIENTS_PER_THREAD * threadNum;
|
||||||
|
for (int i = startIndex; i < startIndex + CLIENTS_PER_THREAD; i++) {
|
||||||
|
oauth.clientId("client" + i);
|
||||||
|
log.trace("Accessing login page for " + oauth.getClientId() + " threat " + threadNum + " iteration " + iterationNum);
|
||||||
|
try {
|
||||||
|
final HttpClientContext context = HttpClientContext.create();
|
||||||
|
|
||||||
|
String pageContent = getPageContent(oauth.getLoginFormUrl(), httpClient, context);
|
||||||
|
String currentUrl = context.getRedirectLocations().get(0).toString();
|
||||||
|
|
||||||
|
Assert.assertTrue(pageContent.contains("<title>AUTH_RESPONSE</title>"));
|
||||||
|
|
||||||
|
String code = getQueryFromUrl(currentUrl).get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse accessRes = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
Assert.assertEquals("AccessTokenResponse: error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
|
||||||
|
200, accessRes.getStatusCode());
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse refreshRes = oauth.doRefreshTokenRequest(accessRes.getRefreshToken(), "password");
|
||||||
|
Assert.assertEquals("AccessTokenResponse: error: '" + refreshRes.getError() + "' desc: '" + refreshRes.getErrorDescription() + "'",
|
||||||
|
200, refreshRes.getStatusCode());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
long end = System.currentTimeMillis() - start;
|
||||||
|
System.out.println("concurrentLogin took " + (end/1000) + "s");
|
||||||
|
System.out.println("*********************************************");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPageContent(String url, CloseableHttpClient httpClient, HttpClientContext context) throws Exception {
|
||||||
|
|
||||||
|
HttpGet request = new HttpGet(url);
|
||||||
|
|
||||||
|
request.setHeader("User-Agent", "Mozilla/5.0");
|
||||||
|
request.setHeader("Accept",
|
||||||
|
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
|
||||||
|
request.setHeader("Accept-Language", "en-US,en;q=0.5");
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
return parseAndCloseResponse(httpClient.execute(request, context));
|
||||||
|
} else {
|
||||||
|
return parseAndCloseResponse(httpClient.execute(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseAndCloseResponse(CloseableHttpResponse response) throws UnsupportedOperationException, IOException {
|
||||||
|
try {
|
||||||
|
int responseCode = response.getStatusLine().getStatusCode();
|
||||||
|
if (responseCode != 200) {
|
||||||
|
log.debug("Response Code : " + responseCode);
|
||||||
|
}
|
||||||
|
BufferedReader rd = new BufferedReader(
|
||||||
|
new InputStreamReader(response.getEntity().getContent()));
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = rd.readLine()) != null) {
|
||||||
|
result.append(line);
|
||||||
|
}
|
||||||
|
if (responseCode != 200) {
|
||||||
|
log.debug(result.toString());
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
} catch (IOException | UnsupportedOperationException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
} finally {
|
||||||
|
if (response != null) {
|
||||||
|
EntityUtils.consumeQuietly(response.getEntity());
|
||||||
|
try {
|
||||||
|
response.close();
|
||||||
|
} catch (IOException ex) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpUriRequest handleLogin(String html, String username, String password) throws UnsupportedEncodingException {
|
||||||
|
|
||||||
|
System.out.println("Extracting form's data...");
|
||||||
|
|
||||||
|
// Keycloak form id
|
||||||
|
Element loginform = Jsoup.parse(html).getElementById("kc-form-login");
|
||||||
|
String method = loginform.attr("method");
|
||||||
|
String action = loginform.attr("action");
|
||||||
|
|
||||||
|
List<NameValuePair> paramList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element inputElement : loginform.getElementsByTag("input")) {
|
||||||
|
String key = inputElement.attr("name");
|
||||||
|
|
||||||
|
if (key.equals("username")) {
|
||||||
|
paramList.add(new BasicNameValuePair(key, username));
|
||||||
|
} else if (key.equals("password")) {
|
||||||
|
paramList.add(new BasicNameValuePair(key, password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPost = method != null && "post".equalsIgnoreCase(method);
|
||||||
|
|
||||||
|
if (isPost) {
|
||||||
|
HttpPost req = new HttpPost(action);
|
||||||
|
|
||||||
|
UrlEncodedFormEntity formEntity;
|
||||||
|
try {
|
||||||
|
formEntity = new UrlEncodedFormEntity(paramList, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
req.setEntity(formEntity);
|
||||||
|
|
||||||
|
return req;
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getQueryFromUrl(String url) throws URISyntaxException {
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
List<NameValuePair> pairs = URLEncodedUtils.parse(new URI(url), "UTF-8");
|
||||||
|
for (NameValuePair p : pairs) {
|
||||||
|
m.put(p.getName(), p.getValue());
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue