Migrate realms if configured to use RH-SSO themes

Closes https://github.com/keycloak/keycloak/issues/17484
This commit is contained in:
rmartinc 2023-04-25 17:39:39 +02:00 committed by Marek Posolda
parent 6c6907ef4e
commit d9025db536
12 changed files with 6051 additions and 32 deletions

View file

@ -0,0 +1,69 @@
/*
* Copyright 2023 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.connections.jpa.updater.liquibase.custom;
import liquibase.exception.CustomChangeException;
import liquibase.statement.core.DeleteStatement;
import liquibase.statement.core.UpdateStatement;
import liquibase.structure.core.Table;
import org.keycloak.theme.DefaultThemeSelectorProvider;
/**
* <p>Migration class to remove old <em>rh-sso</em> themes.</p>
*
* @author rmartinc
*/
public class JpaUpdate22_0_0_RemoveRhssoThemes extends CustomKeycloakTask {
@Override
protected void generateStatementsImpl() throws CustomChangeException {
// remove login theme for realms
statements.add(new UpdateStatement(null, null, database.correctObjectName("REALM", Table.class))
.addNewColumnValue("LOGIN_THEME", null)
.setWhereClause("LOGIN_THEME=?")
.addWhereParameter("rh-sso"));
// remove email theme for realms
statements.add(new UpdateStatement(null, null, database.correctObjectName("REALM", Table.class))
.addNewColumnValue("EMAIL_THEME", null)
.setWhereClause("EMAIL_THEME=?")
.addWhereParameter("rh-sso"));
// remove account theme for realms
statements.add(new UpdateStatement(null, null, database.correctObjectName("REALM", Table.class))
.addNewColumnValue("ACCOUNT_THEME", null)
.setWhereClause("ACCOUNT_THEME=? OR ACCOUNT_THEME=?")
.addWhereParameter("rh-sso")
.addWhereParameter("rh-sso.v2"));
// remove login_theme for clients
if ("oracle".equals(database.getShortName())) {
statements.add(new DeleteStatement(null, null, database.correctObjectName("CLIENT_ATTRIBUTES", Table.class))
.setWhere("NAME=? AND DBMS_LOB.substr(VALUE,10)=?")
.addWhereParameter(DefaultThemeSelectorProvider.LOGIN_THEME_KEY)
.addWhereParameter("rh-sso"));
} else {
statements.add(new DeleteStatement(null, null, database.correctObjectName("CLIENT_ATTRIBUTES", Table.class))
.setWhere("NAME=? AND VALUE=?")
.addWhereParameter(DefaultThemeSelectorProvider.LOGIN_THEME_KEY)
.addWhereParameter("rh-sso"));
}
}
@Override
protected String getTaskId() {
return "Remove RH-SSO themes for keycloak 22.0.0";
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2023 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="22.0.0-17484">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate22_0_0_RemoveRhssoThemes"/>
</changeSet>
</databaseChangeLog>

View file

@ -77,5 +77,6 @@
<include file="META-INF/jpa-changelog-20.0.0.xml"/>
<include file="META-INF/jpa-changelog-21.0.2.xml"/>
<include file="META-INF/jpa-changelog-21.1.0.xml"/>
<include file="META-INF/jpa-changelog-22.0.0.xml"/>
</databaseChangeLog>

View file

@ -181,34 +181,36 @@ mvn -f testsuite/integration-arquillian/pom.xml \
### DB migration test
This test will:
- start MariaDB on docker container. Docker/Podman on your laptop is a requirement for this test.
- start Keycloak 17.0.0 (replace with the other version if needed)
- import realm and add some data to MariaDB
- stop Keycloak 17.0.0
- start latest Keycloak, which automatically updates DB from 17.0.0
- Perform a couple of tests to verify data after the update are correct
The `MigrationTest` test will:
- Start database on docker container. Docker/Podman on your laptop is a requirement for this test.
- Start Keycloak old version 19.0.3.
- Import realm and add some data to the database.
- Stop Keycloak 19.0.3.
- Start latest Keycloak, which automatically updates DB from 19.0.3.
- Perform a couple of tests to verify data after the update are correct.
- Stop MariaDB docker container. In case of a test failure, the MariaDB container is not stopped, so you can manually inspect the database.
The first version of Keycloak on Quarkus is version `17.0.0`.
Therefore, it is not possible to define the older version.
The first version of Keycloak on Quarkus is version `17.0.0`, but the initial versions have a complete different set of boot options that make co-existance impossible.
Therefore the first version that can be tested is `19.0.3`.
You can execute those tests as follows:
```
export OLD_KEYCLOAK_VERSION=17.0.0
export OLD_KEYCLOAK_VERSION=19.0.3
export DATABASE=mariadb
mvn -B -f testsuite/integration-arquillian/pom.xml \
clean install \
-Pjpa,auth-server-quarkus,db-mariadb,auth-server-migration \
-Pjpa,auth-server-quarkus,db-$DATABASE,auth-server-migration \
-Dtest=MigrationTest \
-Dmigration.mode=auto \
-Dmigrated.auth.server.version=$OLD_KEYCLOAK_VERSION \
-Dprevious.product.unpacked.folder.name=keycloak-$OLD_KEYCLOAK_VERSION \
-Dmigration.import.file.name=migration-realm-$OLD_KEYCLOAK_VERSION.json \
-Dauth.server.ssl.required=false \
-Djdbc.mvn.version=2.2.4 \
-Dsurefire.failIfNoSpecifiedTests=false
-Dauth.server.db.host=localhost
```
The `DATABASE` variable can be: `mariadb`, `mysql`, `postgres`, `mssql` or `oracle`.
As commented `OLD_KEYCLOAK_VERSION` can only be `19.0.3` right now.
For the available versions of old keycloak server, you can take a look to [this directory](tests/base/src/test/resources/migration-test) .
### DB migration test with manual mode

View file

@ -154,6 +154,11 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo
commands.add("--http-port=" + configuration.getBindHttpPort());
commands.add("--https-port=" + configuration.getBindHttpsPort());
if (suiteContext.get().isAuthServerMigrationEnabled()) {
commands.add("--hostname-strict=false");
commands.add("--hostname-strict-https=false");
}
if (configuration.getRoute() != null) {
commands.add("-Djboss.node.name=" + configuration.getRoute());
}

View file

@ -141,6 +141,10 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration {
this.providersPath = providersPath;
}
public void setProvidersPath(String providersPath) {
this.providersPath = Paths.get(providersPath);
}
public int getStartupTimeoutInSeconds() {
return startupTimeoutInSeconds;
}

View file

@ -1,6 +1,8 @@
package org.keycloak.testsuite.arquillian.containers;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
@ -10,8 +12,10 @@ import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -63,6 +67,28 @@ public class KeycloakQuarkusServerDeployableContainer extends AbstractQuarkusDep
}
}
private void executeCommand(File wrkDir, String command, String... args) throws IOException {
final List<String> commands = new ArrayList<>();
commands.add(getCommand());
commands.add("-v");
commands.add(command);
if (args != null) {
commands.addAll(Arrays.asList(args));
}
ProcessBuilder pb = new ProcessBuilder(commands);
Process p = pb.directory(wrkDir).inheritIO().start();
try {
if (!p.waitFor(60, TimeUnit.SECONDS)) {
throw new IOException("Command " + command + " did not finished in 60 seconds");
}
if (p.exitValue() != 0) {
throw new IOException("Command " + command + " was executed with exit status " + p.exitValue());
}
} catch (InterruptedException e) {
throw new IOException(e);
}
}
private void importRealm() throws IOException, URISyntaxException {
if (suiteContext.get().isAuthServerMigrationEnabled() && configuration.getImportFile() != null) {
final String importFileName = configuration.getImportFile();
@ -74,14 +100,37 @@ public class KeycloakQuarkusServerDeployableContainer extends AbstractQuarkusDep
final Path path = Paths.get(url.toURI());
final File wrkDir = configuration.getProvidersPath().resolve("bin").toFile();
final List<String> commands = new ArrayList<>();
commands.add(getCommand());
commands.add("import");
commands.add("--file=" + wrkDir.toPath().relativize(path));
Path keycloakConf = Paths.get(wrkDir.toURI()).getParent().resolve("conf").resolve("keycloak.conf");
final ProcessBuilder pb = new ProcessBuilder(commands);
pb.directory(wrkDir).inheritIO().start();
// there are several issues with import in initial quarkus versions, so better use the keycloak.conf file
StoreProvider storeProvider = StoreProvider.getCurrentProvider();
List<String> storageOptions = storeProvider.getStoreOptionsToKeycloakConfImport();
Path keycloakConfBkp = null;
try {
if (!storageOptions.isEmpty()) {
keycloakConfBkp = keycloakConf.getParent().resolve("keycloak.conf.bkp");
Files.copy(keycloakConf, keycloakConfBkp);
// write the options to the file
try ( BufferedWriter w = new BufferedWriter(new FileWriter(keycloakConf.toFile(), true))) {
for (String s : storageOptions) {
w.write(System.lineSeparator());
w.write(s);
}
}
// execute build command to set the storage options if needed
executeCommand(wrkDir, "build");
}
// execute the import
executeCommand(wrkDir, "import", "--file=" + wrkDir.toPath().relativize(path));
} finally {
// restore initial keycloak.conf if modified for import
if (keycloakConfBkp != null && Files.exists(keycloakConfBkp)) {
Files.move(keycloakConfBkp, keycloakConf, StandardCopyOption.REPLACE_EXISTING);
}
}
}
}

View file

@ -19,7 +19,9 @@ package org.keycloak.testsuite.model;
import org.keycloak.utils.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -62,12 +64,25 @@ public enum StoreProvider {
@Override
public void addStoreOptions(List<String> commands) {
getDbVendor().ifPresent(vendor -> commands.add("--db=" + vendor));
commands.add("--db-url='" + System.getProperty("keycloak.connectionsJpa.url") + "'");
commands.add("--db-username=" + System.getProperty("keycloak.connectionsJpa.user"));
commands.add("--db-password=" + System.getProperty("keycloak.connectionsJpa.password"));
if ("mssql".equals(getDbVendor().orElse(null))){
commands.add("--transaction-xa-enabled=false");
}
commands.add("--db-url='" + System.getProperty("keycloak.connectionsJpa.url") + "'");
}
@Override
public List<String> getStoreOptionsToKeycloakConfImport() {
List<String> options = new ArrayList<>();
getDbVendor().ifPresent(vendor -> options.add("db=" + vendor));
options.add("db-url=" + System.getProperty("keycloak.connectionsJpa.url"));
options.add("db-username=" + System.getProperty("keycloak.connectionsJpa.user"));
options.add("db-password=" + System.getProperty("keycloak.connectionsJpa.password"));
if ("mssql".equals(getDbVendor().orElse(null))){
options.add("transaction-xa-enabled=false");
}
return options;
}
},
DEFAULT("default") {
@ -88,6 +103,15 @@ public enum StoreProvider {
this.alias = alias;
}
/**
* Add store options for the import command in migration tests. The options
* will be added as lines in the <em>keycloak.conf</em> file.
* @return The option lines to add
*/
public List<String> getStoreOptionsToKeycloakConfImport() {
return Collections.emptyList();
}
public String getAlias() {
return alias;
}

View file

@ -70,6 +70,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.exportimport.ExportImportUtil;
import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.theme.DefaultThemeSelectorProvider;
import org.keycloak.util.TokenUtil;
import java.io.IOException;
@ -92,7 +93,6 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -155,6 +155,19 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
assertNames(masterRealm.groups().groups(), "master-test-group");
}
protected void testRhssoThemes(RealmResource realm) {
// check themes are removed
RealmRepresentation rep = realm.toRepresentation();
Assert.assertNull("Login theme was not modified", rep.getLoginTheme());
Assert.assertNull("Email theme was not modified", rep.getEmailTheme());
Assert.assertNull("Account theme was not modified", rep.getAccountTheme());
// check the client theme is also removed
List<ClientRepresentation> client = realm.clients().findByClientId("migration-saml-client");
Assert.assertNotNull("migration-saml-client client is missing", client);
Assert.assertEquals("migration-saml-client client is missing", 1, client.size());
Assert.assertNull("migration-saml-client login theme was not removed", client.get(0).getAttributes().get(DefaultThemeSelectorProvider.LOGIN_THEME_KEY));
}
/**
* @see org.keycloak.migration.migrators.MigrateTo2_0_0
*/
@ -328,12 +341,15 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testViewGroups(migrationRealm);
}
protected void testMigrationTo21_0_2() {
testTermsAndConditionsMigrated(masterRealm);
testTermsAndConditionsMigrated(migrationRealm);
testTermsAndConditionsMigrated(migrationRealm2);
}
protected void testMigrationTo21_0_2() {
testTermsAndConditionsMigrated(masterRealm);
testTermsAndConditionsMigrated(migrationRealm);
testTermsAndConditionsMigrated(migrationRealm2);
}
protected void testMigrationTo22_0_0() {
testRhssoThemes(migrationRealm);
}
protected void testDeleteAccount(RealmResource realm) {
ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
@ -1011,6 +1027,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testMigrationTo21_0_2();
}
protected void testMigrationTo22_x() {
testMigrationTo22_0_0();
}
protected void testMigrationTo7_x(boolean supportedAuthzServices) {
if (supportedAuthzServices) {
testDecisionStrategySetOnResourceServer();

View file

@ -20,13 +20,11 @@ import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.migration.Migration;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
/**
@ -59,10 +57,9 @@ public class MigrationTest extends AbstractMigrationTest {
}
@Test
@Migration(versionPrefix = "17.")
public void migration17_xTest() throws Exception{
@Migration(versionPrefix = "19.")
public void migration19_xTest() throws Exception{
testMigratedData(false);
testMigrationTo18_x();
// Always test offline-token login during migration test
testOfflineTokenLogin();
@ -70,5 +67,6 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo20_x();
testMigrationTo21_x();
testMigrationTo22_x();
}
}

View file

@ -729,6 +729,7 @@
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.containers.KeycloakQuarkusServerDeployableContainer</property>
<property name="bindHttpPortOffset">${auth.server.port.offset}</property>
<property name="importFile">${migration.import.file.name}</property>
<property name="providersPath">${keycloak.migration.home}</property>
<property name="javaOpts">
-Xms512m
-Xmx512m