Created
April 19, 2018 20:15
-
-
Save thomasdarimont/3082a1e480cd856b474c1c4498aa3788 to your computer and use it in GitHub Desktop.
Thread-safe user creation with KeycloakAdmin client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>de.tdlabs.keycloak</groupId> | |
<artifactId>keycloak-client-examples</artifactId> | |
<version>1.0.0.BUILD-SNAPSHOT</version> | |
<properties> | |
<maven.compiler.source>1.8</maven.compiler.source> | |
<maven.compiler.target>1.8</maven.compiler.target> | |
<keycloak.version>3.4.3.Final</keycloak.version> | |
<resteasy.version>3.0.24.Final</resteasy.version> | |
</properties> | |
<dependencies> | |
<dependency> | |
<artifactId>keycloak-admin-client</artifactId> | |
<groupId>org.keycloak</groupId> | |
<version>${keycloak.version}</version> | |
</dependency> | |
<dependency> | |
<groupId>org.jboss.resteasy</groupId> | |
<artifactId>resteasy-client</artifactId> | |
<version>${resteasy.version}</version> | |
</dependency> | |
<dependency> | |
<groupId>org.jboss.resteasy</groupId> | |
<artifactId>resteasy-jackson2-provider</artifactId> | |
<version>${resteasy.version}</version> | |
</dependency> | |
</dependencies> | |
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package de.tdlabs.keycloak.client; | |
import java.net.URI; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collections; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Objects; | |
import java.util.concurrent.ExecutorService; | |
import java.util.concurrent.Executors; | |
import java.util.concurrent.Future; | |
import java.util.function.Consumer; | |
import java.util.stream.Collectors; | |
import javax.ws.rs.core.Response; | |
import org.jboss.resteasy.client.jaxrs.ResteasyClient; | |
import org.keycloak.OAuth2Constants; | |
import org.keycloak.admin.client.Keycloak; | |
import org.keycloak.admin.client.KeycloakBuilder; | |
import org.keycloak.representations.idm.CredentialRepresentation; | |
import org.keycloak.representations.idm.UserRepresentation; | |
public class ThreadSafeKeycloakClientExample { | |
public static void main(String[] args) throws Exception { | |
KeycloakClientFacade facade = DefaultKeycloakClientFacade.builder() // | |
.setServerUrl("http://localhost:8080/auth") // | |
.setRealmId("master") // | |
// service account with manage-users role | |
.setClientId("internal-admin-client") // | |
.setClientSecret("7949b07d-e5bf-45f2-91cc-94ca41311371") // | |
.build(); | |
String targetRealm = "demo-many-users"; | |
int threads = 4; | |
int userCount = 25; | |
ExecutorService pool = Executors.newFixedThreadPool(threads); | |
List<Future<?>> futures = new ArrayList<>(); | |
for (int i = 0; i < threads; i++) { | |
futures.add(pool.submit(() -> { | |
for (int j = 0; j < userCount; j++) { | |
UserInfo user = new UserInfo(); | |
long id = System.nanoTime(); | |
user.setUsername("user-" + id); | |
user.setFirstname("First" + id); | |
user.setLastname("Last" + id); | |
user.setEmailAddress("tom+user-" + id + "@localhost"); | |
// create new users in realm: demo-many-users | |
UserReference userRef = facade.createUser(targetRealm, user); | |
System.out.println(userRef); | |
} | |
})); | |
} | |
for (Future<?> f : futures) { | |
f.get(); | |
} | |
pool.shutdownNow(); | |
System.out.println("Found users: " + facade.getUserCount(targetRealm)); | |
System.out.println("Press any key to delete the users again"); | |
System.in.read(); | |
List<String> userIds = new ArrayList<>(); | |
facade.forEachFoundUser(targetRealm, "user-", 100, userInfo -> { | |
System.out.println("Recording user for deletion: " + userInfo); | |
userIds.add(userInfo.getUserId()); | |
}); | |
userIds.stream().forEach(userId -> { | |
System.out.println("Deleting user: " + userId); | |
facade.deleteUser(targetRealm, new UserReference(userId)); | |
}); | |
} | |
interface KeycloakClientFacade { | |
long getUserCount(String realmId); | |
List<UserInfo> listAllUsers(String realmId); | |
UserReference createUser(String realmId, UserInfo userInfo); | |
void deleteUser(String realmId, UserReference userReference); | |
void forEachFoundUser(String realmId, String search, int batchSize, Consumer<UserInfo> consumer); | |
} | |
static class UserInfo extends UserReference { | |
String username; | |
String password; | |
String emailAddress; | |
String firstname; | |
String lastname; | |
Map<String, Object> attributes = Collections.emptyMap(); | |
public UserInfo(String userId) { | |
super(userId); | |
} | |
public UserInfo() { | |
super(UserReference.UNKNOWN_ID); | |
} | |
public String getUsername() { | |
return username; | |
} | |
public void setUsername(String username) { | |
this.username = username; | |
} | |
public String getEmailAddress() { | |
return emailAddress; | |
} | |
public void setEmailAddress(String emailAddress) { | |
this.emailAddress = emailAddress; | |
} | |
public String getFirstname() { | |
return firstname; | |
} | |
public void setFirstname(String firstname) { | |
this.firstname = firstname; | |
} | |
public String getLastname() { | |
return lastname; | |
} | |
public void setLastname(String lastname) { | |
this.lastname = lastname; | |
} | |
public Map<String, Object> getAttributes() { | |
return attributes; | |
} | |
public void setAttributes(Map<String, Object> attributes) { | |
this.attributes = attributes; | |
} | |
@Override | |
public String toString() { | |
return "UserInfo{" + "username='" + username + '\'' + ", emailAddress='" + emailAddress + '\'' | |
+ ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' + ", attributes=" | |
+ attributes + '}'; | |
} | |
} | |
static class UserReference { | |
protected final static String UNKNOWN_ID = "-------------"; | |
private final String userId; | |
public UserReference(URI loc) { | |
this(extractUserId(loc)); | |
} | |
public UserReference(String userId) { | |
this.userId = Objects.equals(userId, UNKNOWN_ID) ? null : Objects.requireNonNull(userId); | |
} | |
private static String extractUserId(URI uri) { | |
String path = uri.getPath(); | |
return path.substring(path.lastIndexOf('/') + 1); | |
} | |
public String getUserId() { | |
return userId; | |
} | |
@Override | |
public String toString() { | |
return "UserReference{" + "userId='" + userId + '\'' + '}'; | |
} | |
} | |
static class DefaultKeycloakClientFacade implements KeycloakClientFacade { | |
private final Keycloak keycloak; | |
public DefaultKeycloakClientFacade(Keycloak keycloak) { | |
this.keycloak = keycloak; | |
} | |
@Override | |
public long getUserCount(String realmId) { | |
return keycloak.realm(realmId).users().count(); | |
} | |
@Override | |
public List<UserInfo> listAllUsers(String realmId) { | |
List<UserRepresentation> results = keycloak.realm(realmId).users().search(null, 0, Integer.MAX_VALUE); | |
return results.stream().map(DefaultKeycloakClientFacade::toUserInfo).collect(Collectors.toList()); | |
} | |
@Override | |
public void forEachFoundUser(String realmId, String search, int batchSize, Consumer<UserInfo> consumer) { | |
int currentIndex = 0; | |
while (true) { | |
List<UserRepresentation> results = keycloak.realm(realmId).users().search(search, currentIndex, | |
batchSize); | |
if (results.isEmpty()) { | |
break; | |
} | |
results.stream().map(DefaultKeycloakClientFacade::toUserInfo).forEach(consumer); | |
if (results.size() < batchSize) { | |
break; | |
} | |
currentIndex += batchSize; | |
} | |
} | |
private static UserInfo toUserInfo(UserRepresentation userRep) { | |
UserInfo userInfo = new UserInfo(userRep.getId()); | |
userInfo.setUsername(userRep.getUsername()); | |
userInfo.setFirstname(userRep.getFirstName()); | |
userInfo.setLastname(userRep.getLastName()); | |
userInfo.setEmailAddress(userRep.getEmail()); | |
userInfo.setAttributes((Map<String, Object>) (Object) userRep.getAttributes()); | |
return userInfo; | |
} | |
@Override | |
public UserReference createUser(String realmId, UserInfo userInfo) { | |
UserRepresentation ur = new UserRepresentation(); | |
ur.setEnabled(true); | |
ur.setUsername(userInfo.getUsername()); | |
ur.setFirstName(userInfo.getFirstname()); | |
ur.setLastName(userInfo.getLastname()); | |
ur.setEmail(userInfo.getEmailAddress()); | |
CredentialRepresentation password = new CredentialRepresentation(); | |
password.setValue("password"); | |
password.setType(CredentialRepresentation.PASSWORD); | |
ur.setCredentials(Arrays.asList(password)); | |
try (ClosableResponseWrapper wrapper = new ClosableResponseWrapper( | |
keycloak.realm(realmId).users().create(ur))) { | |
switch (Response.Status.fromStatusCode(wrapper.getResponse().getStatus())) { | |
case CREATED: | |
return new UserReference(wrapper.getResponse().getLocation()); | |
default: | |
return null; | |
} | |
} | |
} | |
@Override | |
public void deleteUser(String realmId, UserReference userReference) { | |
try (ClosableResponseWrapper wrapper = new ClosableResponseWrapper( | |
keycloak.realm(realmId).users().delete(userReference.getUserId()))) { | |
switch (Response.Status.fromStatusCode(wrapper.getResponse().getStatus())) { | |
case NO_CONTENT: | |
return; | |
default: | |
throw new RuntimeException("DELETE_USER_FAILED"); | |
} | |
} | |
} | |
class ClosableResponseWrapper implements AutoCloseable { | |
private final Response response; | |
public ClosableResponseWrapper(Response response) { | |
this.response = response; | |
} | |
public Response getResponse() { | |
return response; | |
} | |
public void close() { | |
response.close(); | |
} | |
} | |
public static KeycloakClientFacadeBuilder builder() { | |
return new KeycloakClientFacadeBuilder(); | |
} | |
static class KeycloakClientFacadeBuilder { | |
private String serverUrl; | |
private String realmId; | |
private String clientId; | |
private String clientSecret; | |
private String username; | |
private String password; | |
private ResteasyClient resteasyClient; | |
public KeycloakClientFacade build() { | |
KeycloakBuilder builder = username == null ? newKeycloakFromClientCredentials() | |
: newKeycloakFromPasswordCredentials(username, password); | |
if (resteasyClient != null) { | |
builder = builder.resteasyClient(resteasyClient); | |
} | |
return new DefaultKeycloakClientFacade(builder.build()); | |
} | |
private KeycloakBuilder newKeycloakFromClientCredentials() { | |
return KeycloakBuilder.builder() // | |
.realm(realmId) // | |
.serverUrl(serverUrl)// | |
.clientId(clientId) // | |
.clientSecret(clientSecret) // | |
.grantType(OAuth2Constants.CLIENT_CREDENTIALS); | |
} | |
private KeycloakBuilder newKeycloakFromPasswordCredentials(String username, String password) { | |
return newKeycloakFromClientCredentials() // | |
.username(username) // | |
.password(password) // | |
.grantType(OAuth2Constants.PASSWORD); | |
} | |
public KeycloakClientFacadeBuilder setServerUrl(String serverUrl) { | |
this.serverUrl = serverUrl; | |
return this; | |
} | |
public KeycloakClientFacadeBuilder setRealmId(String realmId) { | |
this.realmId = realmId; | |
return this; | |
} | |
public KeycloakClientFacadeBuilder setClientId(String clientId) { | |
this.clientId = clientId; | |
return this; | |
} | |
public KeycloakClientFacadeBuilder setClientSecret(String clientSecret) { | |
this.clientSecret = clientSecret; | |
return this; | |
} | |
public KeycloakClientFacadeBuilder setUsername(String username) { | |
this.username = username; | |
return this; | |
} | |
public KeycloakClientFacadeBuilder setPassword(String password) { | |
this.password = password; | |
return this; | |
} | |
public KeycloakClientFacadeBuilder setResteasyClient(ResteasyClient resteasyClient) { | |
this.resteasyClient = resteasyClient; | |
return this; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment