From 66f487fb46e17dccbaa552b14ffb8d51ff65167f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Fr=C4=85k?= Date: Thu, 7 Nov 2024 17:36:25 +0100 Subject: [PATCH] feat: Update to Keycloak 26 --- CONTRIBUTING.md | 2 +- README.md | 16 ++++++++++++---- docker/.env | 2 +- .../e2e/cypress/e2e/pages/forgotPasswordPage.js | 4 ++-- .../e2e/cypress/e2e/pages/resetPasswordPage.js | 4 ++-- .../e2e/cypress/e2e/pages/updatePasswordPage.js | 4 ++-- .../e2e/cypress/e2e/pages/userFederationPage.js | 14 ++++++++++---- .../e2e/pages/userMigrationProviderPage.js | 14 +++++++++++++- docker/e2e/cypress/e2e/pages/usersPage.js | 8 ++++---- docker/e2e/cypress/support/commands.js | 13 ++++++++++++- pom.xml | 2 +- 11 files changed, 60 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f265bc9..1f8c6118 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,7 @@ When updating the code to work with newer versions of Keycloak, remember to upda To check if the plugin works correctly after the upgrade: 1) Run `mvn clean package` in the project's root directory to run unit tests and build the plugin -2) Run `docker-compose up -d` in `./docker` to create the dependencies necessary for end-to-end testing +2) Run `docker compose up -d` in `./docker` to create the dependencies necessary for end-to-end testing 3) If this is the first time you're doing this, run `npm install` in `./docker/e2e` to install Cypress 4) Run `npx cypress run` in `./docker/e2e` to run end-to-end tests diff --git a/README.md b/README.md index ba8ba082..1086d226 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ https://codesoapbox.dev/keycloak-user-migration *(`SNAPSHOT` means that the version is not yet released)* | Keycloak Version | Version/Commit | -|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| -| 25.X | [5.0.0](https://github.com/daniel-frak/keycloak-user-migration/releases/tag/5.0.0) +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------- +| 26.X | SNAPSHOT | +| 25.X | [5.0.0](https://github.com/daniel-frak/keycloak-user-migration/releases/tag/5.0.0) | 24.X | [4.0.0](https://github.com/daniel-frak/keycloak-user-migration/releases/tag/4.0.0) | | 23.X | [3.0.0](https://github.com/daniel-frak/keycloak-user-migration/releases/tag/3.0.0) | | 22.X | [2.0.0](https://github.com/daniel-frak/keycloak-user-migration/releases/tag/2.0.0) | @@ -39,7 +40,10 @@ https://codesoapbox.dev/keycloak-user-migration ### Note about compatibility with JBoss Keycloak distributions -Using this plugin with legacy JBoss distributions of Keycloak might result in a `java.lang.NoClassDefFoundError: org/apache/commons/codec/binary/Base64` error. It seems that [adding the maven-shade-plugin](https://github.com/daniel-frak/keycloak-user-migration/issues/72) as a dependency fixes this issue. +Using this plugin with legacy JBoss distributions of Keycloak might result in a +`java.lang.NoClassDefFoundError: org/apache/commons/codec/binary/Base64` error. It seems +that [adding the maven-shade-plugin](https://github.com/daniel-frak/keycloak-user-migration/issues/72) as a dependency +fixes this issue. ## Prerequisites - REST endpoints in the legacy system @@ -318,7 +322,9 @@ This switch can be toggled to decide whether groups which are not defined in the migrated anyway or simply ignored. ## Totp + This module supports the migration of totp devices. The totp configuration block could look like this: + ```json { "name": "Totp Device 1", @@ -329,6 +335,8 @@ This module supports the migration of totp devices. The totp configuration block "encoding": "BASE32" } ``` -`name` should be the name of the totp device, while `secret` is the secret, that could be encoded in "BASE32" or as UTF-8 plaintext. + +`name` should be the name of the totp device, while `secret` is the secret, that could be encoded in "BASE32" or as +UTF-8 plaintext. For the utf8 bytes just set the `encoding` attribute to null. Possible `algorithm`s are: HmacSHA1, HmacSHA256, HmacSHA512 \ No newline at end of file diff --git a/docker/.env b/docker/.env index 3800182c..e21f3e17 100644 --- a/docker/.env +++ b/docker/.env @@ -1,5 +1,5 @@ COMPOSE_PROJECT_NAME=keycloak_migration_demo -KEYCLOAK_IMAGE=quay.io/keycloak/keycloak:25.0.0 +KEYCLOAK_IMAGE=quay.io/keycloak/keycloak:26.0.5 MAVEN_IMAGE=maven:3.9.7-eclipse-temurin-21 OPENJDK_IMAGE=openjdk:21-jdk-slim diff --git a/docker/e2e/cypress/e2e/pages/forgotPasswordPage.js b/docker/e2e/cypress/e2e/pages/forgotPasswordPage.js index b9f3fbb4..374ef1e7 100644 --- a/docker/e2e/cypress/e2e/pages/forgotPasswordPage.js +++ b/docker/e2e/cypress/e2e/pages/forgotPasswordPage.js @@ -2,7 +2,7 @@ class ForgotPasswordPage { elements = { userNameInput: () => cy.get('#username'), - submitBtn: () => cy.get('input[type=submit]') + form: () => cy.get('#kc-reset-password-form') } visit() { @@ -11,7 +11,7 @@ class ForgotPasswordPage { triggerPasswordReset(userEmail) { this.elements.userNameInput().clear().type(userEmail); - this.elements.submitBtn().click(); + this.elements.form().submit(); cy.get('body').should('contain.text', 'You should receive an email shortly with further instructions.'); cy.mhGetMailsBySubject('Reset password') diff --git a/docker/e2e/cypress/e2e/pages/resetPasswordPage.js b/docker/e2e/cypress/e2e/pages/resetPasswordPage.js index 2b5e2c4f..97b522a3 100644 --- a/docker/e2e/cypress/e2e/pages/resetPasswordPage.js +++ b/docker/e2e/cypress/e2e/pages/resetPasswordPage.js @@ -3,13 +3,13 @@ class ResetPasswordPage { elements = { passwordInput: () => cy.get('#password-new'), confirmPasswordInput: () => cy.get('#password-confirm'), - submitBtn: () => cy.get('input[type=submit]') + form: () => cy.get('#kc-passwd-update-form'), } chooseNewPassword(newPassword) { this.elements.passwordInput().type(newPassword); this.elements.confirmPasswordInput().type(newPassword); - this.elements.submitBtn().click(); + this.elements.form().submit(); } } diff --git a/docker/e2e/cypress/e2e/pages/updatePasswordPage.js b/docker/e2e/cypress/e2e/pages/updatePasswordPage.js index a40a7da2..67c27bd1 100644 --- a/docker/e2e/cypress/e2e/pages/updatePasswordPage.js +++ b/docker/e2e/cypress/e2e/pages/updatePasswordPage.js @@ -3,13 +3,13 @@ class UpdatePasswordPage { elements = { passwordInput: () => cy.get('#password-new'), confirmPasswordInput: () => cy.get('#password-confirm'), - submitBtn: () => cy.get('input[type=submit]') + form: () => cy.get('#kc-passwd-update-form'), } chooseNewPassword(newPassword) { this.elements.passwordInput().type(newPassword); this.elements.confirmPasswordInput().type(newPassword); - this.elements.submitBtn().click(); + this.elements.form().submit() } } diff --git a/docker/e2e/cypress/e2e/pages/userFederationPage.js b/docker/e2e/cypress/e2e/pages/userFederationPage.js index d8b99120..904b3851 100644 --- a/docker/e2e/cypress/e2e/pages/userFederationPage.js +++ b/docker/e2e/cypress/e2e/pages/userFederationPage.js @@ -24,11 +24,17 @@ class UserFederationPage { } visit() { - cy.intercept('/admin/realms/master/components*') - .as("components") + cy.intercept('/admin/realms/master/components*&type=org.keycloak.storage.UserStorageProvider*') + .as("getUserStorageProviders") cy.visit('/admin/master/console/#/master/user-federation'); - cy.wait('@components'); - cy.wait(2000); // The initial page will always claim there are no components, so we must wait to make sure. + cy.wait('@getUserStorageProviders'); + + /* + The initial page will always claim there are no configured User Federation Providers, + and there is no way to check whether it has updated the DOM using the @getUserStorageProviders request. + Therefore, an artificial wait is introduced to make sure the DOM has updated. + */ + cy.wait(5000); } removePluginIfExists() { diff --git a/docker/e2e/cypress/e2e/pages/userMigrationProviderPage.js b/docker/e2e/cypress/e2e/pages/userMigrationProviderPage.js index 7789b8e2..3ff2c969 100644 --- a/docker/e2e/cypress/e2e/pages/userMigrationProviderPage.js +++ b/docker/e2e/cypress/e2e/pages/userMigrationProviderPage.js @@ -7,6 +7,9 @@ class UserMigrationProviderPage { header: () => cy.get("h1"), uiDisplayName: () => cy.getByTestId('name'), restClientUri: () => cy.getByTestId('URI'), + actionDropdown: () => cy.getByTestId('action-dropdown'), + actionDropdownRemoveImportedBtn: () => cy.get('button').contains('Remove imported'), + modalConfirmButton: () => cy.getByTestId('confirm'), saveBtn: () => cy.get('button').contains('Save') } @@ -15,7 +18,7 @@ class UserMigrationProviderPage { userFederationPage.goToUserMigrationPluginPage(data.providerName, data.pluginName); } - addPlugin(legacySystemUrl) { + configurePlugin(legacySystemUrl) { this.elements.uiDisplayName() .invoke('val', '') // clear() doesn't seem to work here for some reason .type(data.pluginName); @@ -27,6 +30,15 @@ class UserMigrationProviderPage { this.elements.saveBtn().click() cy.wait('@savePlugin').its('response.statusCode').should('be.oneOf', [201, 204]); } + + removeImportedUsers() { + this.elements.actionDropdown().click(); + this.elements.actionDropdownRemoveImportedBtn().click(); + cy.intercept('POST', '/admin/realms/master/user-storage/*/remove-imported-users') + .as('removeImportedUsers') + this.elements.modalConfirmButton().click(); + cy.wait('@removeImportedUsers'); + } } module.exports = new UserMigrationProviderPage(); \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/pages/usersPage.js b/docker/e2e/cypress/e2e/pages/usersPage.js index 3f08fb87..d3cbfcb3 100644 --- a/docker/e2e/cypress/e2e/pages/usersPage.js +++ b/docker/e2e/cypress/e2e/pages/usersPage.js @@ -15,10 +15,10 @@ class UsersPage { } visit() { - cy.intercept('/admin/realms/master/components*') - .as("components") + cy.intercept('admin/realms/master/ui-ext/brute-force-user*') + .as("userList") cy.visit('/admin/master/console/#/master/users'); - cy.wait('@components'); + cy.wait('@userList'); } goToUserDetails(userName) { @@ -68,7 +68,7 @@ class UsersPage { assertUserWasDeleted(userName) { return this.findByName(userName) - .then((() => this.elements.foundUserTable().should('not.exist'))) + .then((() => this.elements.foundUserTable(userName).should('not.exist'))) .then((() => this.elements.emptySearchResultsText().should('exist'))); } } diff --git a/docker/e2e/cypress/support/commands.js b/docker/e2e/cypress/support/commands.js index c65101b6..d2388d36 100644 --- a/docker/e2e/cypress/support/commands.js +++ b/docker/e2e/cypress/support/commands.js @@ -56,7 +56,7 @@ Cypress.Commands.add("setupKeycloak", () => { userFederationPage.visit(); userFederationPage.removePluginIfExists(); userFederationPage.goToUserMigrationPluginPage(); - userMigrationProviderPage.addPlugin(data.legacySystem.url); + userMigrationProviderPage.configurePlugin(data.legacySystem.url); } /** @@ -88,10 +88,21 @@ Cypress.Commands.add("resetState", () => { } function deleteTestUserIfExists() { + removeImportedUsers(); usersPage.visit(); return usersPage.deleteUserIfExists(data.legacyUser.username); } + /* + Users which still have a federation link (e.g. they have been imported but have not successfully logged in yet) + cannot be removed using normal means. The "Remove imported users" option on the User Federation Plugin page + removes those users (but will not remove users for whom the federation link has already been severed). + */ + function removeImportedUsers() { + userMigrationProviderPage.visit(); + userMigrationProviderPage.removeImportedUsers(); + } + function deletePasswordPoliciesIfExist() { passwordPoliciesPage.visit(); return passwordPoliciesPage.deleteEveryPasswordPolicy(); diff --git a/pom.xml b/pom.xml index 6e6639c3..26444a50 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ 5.1.1-SNAPSHOT 21 - 25.0.0 + 26.0.5 5.14.2 ${java.version}