Migrate realms if configured to use RH-SSO themes
Closes https://github.com/keycloak/keycloak/issues/17484
This commit is contained in:
parent
6c6907ef4e
commit
d9025db536
12 changed files with 6051 additions and 32 deletions
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue