Skip to content

Commit

Permalink
Update HttpClient behaviour to prevent client pool leaks
Browse files Browse the repository at this point in the history
  • Loading branch information
y-a-n-n committed Feb 17, 2021
1 parent c7bcacb commit 1096fa8
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
- run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Build
run: gradle build
- name: Create release
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ To be used on a Keycloak instance configured with with the [Keycloak Firebase Sc
## Download latest release

```
curl -L https://github.com/SmartMoveSystems/firebase-keycloak-importer/releases/download/0.0.4/firebase-keycloak-importer-0.0.4.jar > firebase-keycloak-importer-0.0.4.jar
curl -L https://github.com/SmartMoveSystems/firebase-keycloak-importer/releases/download/0.0.5/firebase-keycloak-importer-0.0.5.jar > firebase-keycloak-importer-0.0.5.jar
```

## Usage
Expand All @@ -36,13 +36,13 @@ Export your Firebase project's hash parameters to a JSON file with the format:

Run the following (the args `"--clientId", "--roles", "--clientSecret", "--default", "--debug"` are optional):

The `default` argument specifies that the imported hash parameters will be the ones used for future users.
If you are only importing from one Firebase project, you must set this argument to `true`.

```bash
java -jar build\libs\firebase-keycloak-importer-0.0.4.jar --usersFile example_users.json --hashParamsFile example_hash_config.json --adminUser [email protected] --adminPassword admin --realm smartmove --serverUrl http://localhost:8080/auth --default true --debug
java -jar build\libs\firebase-keycloak-importer-0.0.5.jar --usersFile example_users.json --hashParamsFile example_hash_config.json --adminUser [email protected] --adminPassword admin --realm smartmove --serverUrl http://localhost:8080/auth --default true
```

The `default` argument specifies that the imported hash parameters will be the ones used for future users.
If you are only importing from one Firebase project, you must set this argument to `true`.

The client with the specified `clientId` and all specified `roles` must already exist in your Keycloak configuration.

## Importing from multiple projects
Expand Down
4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = "com.smartmovesystems.keycloak.firebasemigrator"
version = "0.0.4"
version = "0.0.5"

repositories {
mavenCentral()
Expand All @@ -21,6 +21,8 @@ dependencies {
implementation("org.keycloak:keycloak-admin-client:$keycloakVersion")
implementation("com.squareup.moshi:moshi:1.9.3")
implementation("com.squareup.moshi:moshi-kotlin:1.9.3")
implementation("org.apache.httpcomponents:httpcore-nio:4.4.14")
implementation("org.apache.httpcomponents:httpasyncclient:4.1.4")
implementation("org.json:json:20200518")
compileOnly("org.keycloak:keycloak-core:$keycloakVersion")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package com.smartmovesystems.keycloak.firebasemigrator

import com.smartmovesystems.keycloak.firebasemigrator.model.*
import org.json.JSONObject
import org.keycloak.OAuth2Constants
import org.keycloak.admin.client.CreatedResponseUtil
import org.keycloak.admin.client.KeycloakBuilder
import org.keycloak.admin.client.*
import org.keycloak.admin.client.resource.RealmResource
import org.keycloak.admin.client.resource.UserResource
import org.keycloak.admin.client.resource.UsersResource
Expand All @@ -27,21 +25,9 @@ fun createUsers(arguments: Arguments) {
val users = parseFile<UserList>(arguments.fileName)

if (users != null) {

log.info("Found ${users.users.size} users in file.")
log.info("About to connect to Keycloak instance at ${arguments.serverUrl} as user ${arguments.adminUser}")
val keycloak = KeycloakBuilder.builder()
.serverUrl(arguments.serverUrl)
.realm(arguments.realm)
.grantType(OAuth2Constants.PASSWORD)
.clientId("admin-cli")
.clientSecret(arguments.clientSecret)
.username(arguments.adminUser)
.password(arguments.adminPassword)
.build()

val realmResource = keycloak.realm(arguments.realm)
val usersResource = realmResource.users()
val keycloak = getKcInstance(arguments)

val hashParamsId = parseFile<FirebaseHashConfig>(arguments.hashParamsFile)?.let {
log.info("Adding scrypt hash parameters...")
Expand All @@ -52,6 +38,8 @@ fun createUsers(arguments: Arguments) {
var created = 0

users.users.forEach {
val realmResource = keycloak.realm(arguments.realm)
val usersResource = realmResource.users()
createOneUser(it, usersResource, hashParamsId)?.let { user ->
addUserRoles(user, realmResource, arguments.clientId, arguments.roles ?: emptyList())
created++
Expand All @@ -63,7 +51,7 @@ fun createUsers(arguments: Arguments) {
}
}

fun setLogLevel(debug: Boolean) {
private fun setLogLevel(debug: Boolean) {
val rootLogger: Logger = LogManager.getLogManager().getLogger("")
val level = if (debug) Level.FINE else Level.INFO
rootLogger.level = level
Expand All @@ -72,7 +60,7 @@ fun setLogLevel(debug: Boolean) {
}
}

fun createOneUser(user: FirebaseUser, usersResource: UsersResource, hashParamsId: String?): UserResource? {
private fun createOneUser(user: FirebaseUser, usersResource: UsersResource, hashParamsId: String?): UserResource? {
log.fine("Creating user ${user.email}...")
val keycloakUser = user.convert()
return try {
Expand All @@ -89,12 +77,12 @@ fun createOneUser(user: FirebaseUser, usersResource: UsersResource, hashParamsId
} else {
null
}

}
}

fun duplicateEmail(user: FirebaseUser, usersResource: UsersResource): UserResource? {
private fun duplicateEmail(user: FirebaseUser, usersResource: UsersResource): UserResource? {
log.info("Trying to find existing user with email ${user.email}")
// The user search has a tendency to hang so ensure client is alive first
val users = usersResource.search(user.email, true)
return if (users.size == 1) {
log.info("Found the user with id ${users[0].id}")
Expand All @@ -115,7 +103,7 @@ fun duplicateEmail(user: FirebaseUser, usersResource: UsersResource): UserResour
}
}

fun addUserCredential(userResource: UserResource, user: FirebaseUser, hashParamsId: String?) {
private fun addUserCredential(userResource: UserResource, user: FirebaseUser, hashParamsId: String?) {
val credential = CredentialRepresentation()
val representation = userResource.toRepresentation()
credential.isTemporary = false
Expand All @@ -126,7 +114,7 @@ fun addUserCredential(userResource: UserResource, user: FirebaseUser, hashParams
userResource.update(representation)
}

fun addUserRoles(userResource: UserResource, realmResource: RealmResource, clientId: String?, roles: List<String>) {
private fun addUserRoles(userResource: UserResource, realmResource: RealmResource, clientId: String?, roles: List<String>) {

log.fine("Adding roles for user...")

Expand Down Expand Up @@ -154,7 +142,7 @@ fun addUserRoles(userResource: UserResource, realmResource: RealmResource, clien
}
}

fun FirebaseUser.convert() : UserRepresentation {
private fun FirebaseUser.convert() : UserRepresentation {
val user = UserRepresentation()
user.isEnabled = true
user.username = email
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.smartmovesystems.keycloak.firebasemigrator

import org.jboss.resteasy.client.jaxrs.ResteasyClient
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget
import org.keycloak.OAuth2Constants
import org.keycloak.admin.client.JacksonProvider
import org.keycloak.admin.client.Keycloak
import org.keycloak.admin.client.KeycloakBuilder
import javax.ws.rs.client.ClientRequestContext
import javax.ws.rs.client.ClientRequestFilter

private val Keycloak.webTarget : ResteasyWebTarget
get() {
val targetField = this::class.java.getDeclaredField("target")
targetField.isAccessible = true
return targetField.get(this) as ResteasyWebTarget
}

/**
* Making repeated calls to the RestEasyClient seems to eventually exhaust the client pool, regardless of size and
* configuration. Therefore, request the server close connections aggressively to avoid hangs
*/
private fun Keycloak.setCloseConnections() {
this.webTarget.register(object : ClientRequestFilter {
override fun filter(requestContext: ClientRequestContext?) {
requestContext!!.headers.add("Connection", "close")
}
})
}

private val resteasyClientBuilder: ResteasyClientBuilder by lazy {
ResteasyClientBuilder()
.register(JacksonProvider::class.java, 100)
}

private val restEasyClient: ResteasyClient by lazy {
resteasyClientBuilder.useAsyncHttpEngine().build()
}

/**
* Returns a logged-in Keycloak instance based on the provided arguments
*/
fun getKcInstance(arguments: Arguments): Keycloak {
val keycloak = KeycloakBuilder.builder()
.serverUrl(arguments.serverUrl)
.realm(arguments.realm)
.grantType(OAuth2Constants.PASSWORD)
.clientId("admin-cli")
.clientSecret(arguments.clientSecret)
.resteasyClient(restEasyClient)
.username(arguments.adminUser)
.password(arguments.adminPassword)
.build()
keycloak.setCloseConnections()
return keycloak
}

0 comments on commit 1096fa8

Please sign in to comment.