Skip to content

Commit

Permalink
Update behaviour for duplicate users between projects
Browse files Browse the repository at this point in the history
  • Loading branch information
y-a-n-n committed Feb 15, 2021
1 parent ff4538a commit c7bcacb
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 23 deletions.
21 changes: 9 additions & 12 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.3/firebase-keycloak-importer-0.0.3.jar > firebase-keycloak-importer-0.0.3.jar
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
```

## Usage
Expand All @@ -34,22 +34,19 @@ Export your Firebase project's hash parameters to a JSON file with the format:
}
```

Run the following (the args `"--clientId", "--roles", "--clientSecret", "--default"` are optional):
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 firebase-keycloak-importer-0.0.3.jar --usersFile example_users.json \
--hashParamsFile example_hash_config.json \
--adminUser admin \
--adminPassword admin \
--realm master \
--serverUrl http://localhost:8080/auth \
--clientId your_keycloak_client_app_id \
--roles oneRole,anotherRole \
--clientSecret 0d61686d-57fc-4048-b052-4ce74978c468 \
--default true
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
```

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

## Importing from multiple projects

If there is a user with the same email address between multiple imported projects, the first imported user record wins.

The only difference is that the `phone_verified` claim is set to false if the imported phone numbers differ between projects.
2 changes: 1 addition & 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.3"
version = "0.0.4"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ data class Arguments(
val clientId: String?,
val roles: List<String>?,
val clientSecret: String?,
val default: Boolean
val default: Boolean,
val debug: Boolean
) {

companion object {
Expand All @@ -25,7 +26,7 @@ data class Arguments(
"--adminPassword",
"--serverUrl"
)
val OPTIONAL_ARGS = arrayOf( "--clientId", "--roles", "--clientSecret", "--default")
val OPTIONAL_ARGS = arrayOf( "--clientId", "--roles", "--clientSecret", "--default", "--debug")
}
}

Expand Down Expand Up @@ -58,7 +59,8 @@ fun fromStringArray(args: Array<String>): Arguments {
argMap[Arguments.OPTIONAL_ARGS[0]],
argMap[Arguments.OPTIONAL_ARGS[1]]?.split(","),
argMap[Arguments.OPTIONAL_ARGS[2]],
argMap[Arguments.OPTIONAL_ARGS[3]]?.toBoolean() ?: false
argMap[Arguments.OPTIONAL_ARGS[3]]?.toBoolean() ?: false,
argMap[Arguments.OPTIONAL_ARGS[4]]?.toBoolean() ?: false
)
} catch (e: Exception) {
throw IllegalArgumentException("Badly formatted argument", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ import org.keycloak.representations.idm.ClientRepresentation
import org.keycloak.representations.idm.CredentialRepresentation
import org.keycloak.representations.idm.UserRepresentation
import java.lang.Exception
import java.util.logging.Level
import java.util.logging.LogManager
import java.util.logging.Logger
import javax.ws.rs.core.Response

private val log = Logger.getLogger("createUsers")

fun createUsers(arguments: Arguments) {

setLogLevel(arguments.debug)

log.info("Parsing users file...")
val users = parseFile<UserList>(arguments.fileName)

Expand All @@ -44,29 +48,69 @@ fun createUsers(arguments: Arguments) {
createHashParameters(keycloak, arguments.realm, arguments.serverUrl, it.hash_config, arguments.default)
}

val size = users.users.size
var created = 0

users.users.forEach {
createOneUser(it, usersResource, hashParamsId)?.let { user ->
addUserRoles(user, realmResource, arguments.clientId, arguments.roles ?: emptyList())
created++
}
}
log.info("Users added")
log.info("$created of $size users added")
} else {
log.warning("No users found in file")
}
}

fun setLogLevel(debug: Boolean) {
val rootLogger: Logger = LogManager.getLogManager().getLogger("")
val level = if (debug) Level.FINE else Level.INFO
rootLogger.level = level
for (h in rootLogger.handlers) {
h.level = level
}
}

fun createOneUser(user: FirebaseUser, usersResource: UsersResource, hashParamsId: String?): UserResource? {
log.info("Creating user ${user.email}...")
log.fine("Creating user ${user.email}...")
val keycloakUser = user.convert()
return try {
val response: Response = usersResource.create(keycloakUser)
val userId = CreatedResponseUtil.getCreatedId(response)
val resource = usersResource.get(userId)
addUserCredential(resource, user, hashParamsId)
log.info("Created user ID $userId")
log.fine("Created user ID $userId")
usersResource.get(userId)
} catch (e: Exception) {
log.warning("Error creating user: ${e.message}")
log.warning("Error creating user: $e")
if (e.message?.contains("Create method returned status Conflict") == true) {
duplicateEmail(user, usersResource)
} else {
null
}

}
}

fun duplicateEmail(user: FirebaseUser, usersResource: UsersResource): UserResource? {
log.info("Trying to find existing user with email ${user.email}")
val users = usersResource.search(user.email, true)
return if (users.size == 1) {
log.info("Found the user with id ${users[0].id}")
val phoneNumber = users[0].attributes?.get("phone_number")?.firstOrNull()
val existing = usersResource.get(users[0].id)
if (phoneNumber != user.phoneNumber) {
log.info("User's phone number doesn't match; setting to invalid")
val representation = existing.toRepresentation()
representation.singleAttribute("phone_verified", "false")
existing.update(representation)
} else {
log.info("Phone numbers match; nothing to do here")
}
existing
} else {
log.severe("There were ${users.size} users with this email address. Cannot update.")
null
}
}
Expand All @@ -84,7 +128,7 @@ fun addUserCredential(userResource: UserResource, user: FirebaseUser, hashParams

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

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

// Add realm-level offline_access role to allow refresh token flow
val role = realmResource.roles()["offline_access"].toRepresentation()
Expand All @@ -95,13 +139,13 @@ fun addUserRoles(userResource: UserResource, realmResource: RealmResource, clien
val appClient: ClientRepresentation = realmResource.clients()
.findByClientId(id)[0]

log.info("Found appClient: ${appClient.id}")
log.fine("Found appClient: ${appClient.id}")

roles.forEach {
val userClientRole = realmResource.clients()[appClient.id]
.roles()[it].toRepresentation()

log.info("Found userClientRole: ${userClientRole.id}")
log.fine("Found userClientRole: ${userClientRole.id}")

// Assign client level role to user
userResource.roles()
Expand Down

0 comments on commit c7bcacb

Please sign in to comment.