KEYCLOAK-9129 Don't expose Keycloak version in resource paths

This commit is contained in:
stianst 2019-11-12 14:25:03 +01:00 committed by Stian Thorgersen
parent b72fe79791
commit 3a36569e20
9 changed files with 193 additions and 27 deletions

View file

@ -31,6 +31,7 @@ public class Version {
public static String NAME_FULL; public static String NAME_FULL;
public static String NAME_HTML; public static String NAME_HTML;
public static String VERSION; public static String VERSION;
public static String VERSION_KEYCLOAK;
public static String RESOURCES_VERSION; public static String RESOURCES_VERSION;
public static String BUILD_TIME; public static String BUILD_TIME;
public static String DEFAULT_PROFILE; public static String DEFAULT_PROFILE;
@ -45,6 +46,7 @@ public class Version {
Version.NAME_HTML = props.getProperty("name-html"); Version.NAME_HTML = props.getProperty("name-html");
Version.DEFAULT_PROFILE = props.getProperty("default-profile"); Version.DEFAULT_PROFILE = props.getProperty("default-profile");
Version.VERSION = props.getProperty("version"); Version.VERSION = props.getProperty("version");
Version.VERSION_KEYCLOAK = props.getProperty("version-keycloak");
Version.BUILD_TIME = props.getProperty("build-time"); Version.BUILD_TIME = props.getProperty("build-time");
Version.RESOURCES_VERSION = Version.VERSION.toLowerCase(); Version.RESOURCES_VERSION = Version.VERSION.toLowerCase();

View file

@ -19,5 +19,6 @@ name=${product.name}
name-full=${product.name.full} name-full=${product.name.full}
name-html=${product.name-html} name-html=${product.name-html}
version=${product.version} version=${product.version}
version-keycloak=${project.version}
build-time=${product.build-time} build-time=${product.build-time}
default-profile=${product.default-profile} default-profile=${product.default-profile}

View file

@ -17,10 +17,14 @@
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import org.keycloak.common.util.Time;
import org.keycloak.migration.MigrationModel; import org.keycloak.migration.MigrationModel;
import org.keycloak.models.jpa.entities.MigrationModelEntity; import org.keycloak.models.jpa.entities.MigrationModelEntity;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.security.SecureRandom;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -28,29 +32,62 @@ import javax.persistence.EntityManager;
*/ */
public class MigrationModelAdapter implements MigrationModel { public class MigrationModelAdapter implements MigrationModel {
protected EntityManager em; protected EntityManager em;
protected MigrationModelEntity latest;
private static final int RESOURCE_TAG_LENGTH = 5;
private static final char[] RESOURCE_TAG_CHARSET = "0123456789abcdefghijklmnopqrstuvwxyz".toCharArray();
public MigrationModelAdapter(EntityManager em) { public MigrationModelAdapter(EntityManager em) {
this.em = em; this.em = em;
init();
} }
@Override @Override
public String getStoredVersion() { public String getStoredVersion() {
MigrationModelEntity entity = em.find(MigrationModelEntity.class, MigrationModelEntity.SINGLETON_ID); return latest != null ? latest.getVersion() : null;
if (entity == null) return null; }
return entity.getVersion();
@Override
public String getResourcesTag() {
return latest != null ? latest.getId() : null;
}
private void init() {
TypedQuery<MigrationModelEntity> q = em.createNamedQuery("getLatest", MigrationModelEntity.class);
q.setMaxResults(1);
List<MigrationModelEntity> l = q.getResultList();
if (l.isEmpty()) {
latest = null;
} else {
latest = l.get(0);
}
} }
@Override @Override
public void setStoredVersion(String version) { public void setStoredVersion(String version) {
MigrationModelEntity entity = em.find(MigrationModelEntity.class, MigrationModelEntity.SINGLETON_ID); String resourceTag = createResourceTag();
if (entity == null) {
entity = new MigrationModelEntity(); // Make sure resource-tag is unique within current installation
entity.setId(MigrationModelEntity.SINGLETON_ID); while (em.find(MigrationModelEntity.class, resourceTag) != null) {
resourceTag = createResourceTag();
}
MigrationModelEntity entity = new MigrationModelEntity();
entity.setId(resourceTag);
entity.setVersion(version); entity.setVersion(version);
entity.setUpdatedTime(Time.currentTime());
em.persist(entity); em.persist(entity);
} else {
entity.setVersion(version); latest = entity;
em.flush();
} }
private String createResourceTag() {
StringBuilder sb = new StringBuilder(RESOURCE_TAG_LENGTH);
for (int i = 0; i < RESOURCE_TAG_LENGTH; i++) {
sb.append(RESOURCE_TAG_CHARSET[new SecureRandom().nextInt(RESOURCE_TAG_CHARSET.length)]);
} }
return sb.toString();
}
} }

View file

@ -22,7 +22,10 @@ import javax.persistence.AccessType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.Date;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -30,6 +33,9 @@ import javax.persistence.Table;
*/ */
@Table(name="MIGRATION_MODEL") @Table(name="MIGRATION_MODEL")
@Entity @Entity
@NamedQueries({
@NamedQuery(name = "getLatest", query = "select m from MigrationModelEntity m ORDER BY m.updatedTime DESC")
})
public class MigrationModelEntity { public class MigrationModelEntity {
public static final String SINGLETON_ID = "SINGLETON"; public static final String SINGLETON_ID = "SINGLETON";
@Id @Id
@ -40,6 +46,9 @@ public class MigrationModelEntity {
@Column(name="VERSION", length = 36) @Column(name="VERSION", length = 36)
protected String version; protected String version;
@Column(name="UPDATE_TIME")
protected long updatedTime;
public String getId() { public String getId() {
return id; return id;
} }
@ -56,6 +65,14 @@ public class MigrationModelEntity {
this.version = version; this.version = version;
} }
public long getUpdateTime() {
return updatedTime;
}
public void setUpdatedTime(long updatedTime) {
this.updatedTime = updatedTime;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -202,4 +202,16 @@
</changeSet> </changeSet>
<changeSet author="keycloak" id="8.0.0-resource-tag-support">
<addColumn tableName="MIGRATION_MODEL">
<column name="UPDATE_TIME" type="BIGINT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</addColumn>
<createIndex tableName="MIGRATION_MODEL" indexName="IDX_UPDATE_TIME">
<column name="UPDATE_TIME" type="BIGINT" />
</createIndex>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -21,6 +21,7 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.Version;
import org.keycloak.migration.migrators.MigrateTo1_2_0; import org.keycloak.migration.migrators.MigrateTo1_2_0;
import org.keycloak.migration.migrators.MigrateTo1_3_0; import org.keycloak.migration.migrators.MigrateTo1_3_0;
import org.keycloak.migration.migrators.MigrateTo1_4_0; import org.keycloak.migration.migrators.MigrateTo1_4_0;
@ -87,26 +88,28 @@ public class MigrationModelManager {
}; };
public static void migrate(KeycloakSession session) { public static void migrate(KeycloakSession session) {
ModelVersion latest = migrations[migrations.length-1].getVersion();
MigrationModel model = session.realms().getMigrationModel(); MigrationModel model = session.realms().getMigrationModel();
ModelVersion stored = null;
if (model.getStoredVersion() != null) {
stored = new ModelVersion(model.getStoredVersion());
if (latest.equals(stored)) {
return;
}
}
ModelVersion currentVersion = new ModelVersion(Version.VERSION_KEYCLOAK);
ModelVersion latestUpdate = migrations[migrations.length-1].getVersion();
ModelVersion databaseVersion = model.getStoredVersion() != null ? new ModelVersion(model.getStoredVersion()) : null;
if (databaseVersion == null || databaseVersion.lessThan(latestUpdate)) {
for (Migration m : migrations) { for (Migration m : migrations) {
if (stored == null || stored.lessThan(m.getVersion())) { if (databaseVersion == null || databaseVersion.lessThan(m.getVersion())) {
if (stored != null) { if (databaseVersion != null) {
logger.debugf("Migrating older model to %s", m.getVersion()); logger.debugf("Migrating older model to %s", m.getVersion());
} }
m.migrate(session); m.migrate(session);
} }
} }
}
model.setStoredVersion(latest.toString()); if (databaseVersion == null || databaseVersion.lessThan(currentVersion)) {
model.setStoredVersion(currentVersion.toString());
}
Version.RESOURCES_VERSION = model.getResourcesTag();
} }
public static final ModelVersion RHSSO_VERSION_7_0_KEYCLOAK_VERSION = new ModelVersion("1.9.8"); public static final ModelVersion RHSSO_VERSION_7_0_KEYCLOAK_VERSION = new ModelVersion("1.9.8");

View file

@ -24,5 +24,6 @@ package org.keycloak.migration;
*/ */
public interface MigrationModel { public interface MigrationModel {
String getStoredVersion(); String getStoredVersion();
String getResourcesTag();
void setStoredVersion(String version); void setStoredVersion(String version);
} }

View file

@ -16,11 +16,15 @@
*/ */
package org.keycloak.testsuite.migration; package org.keycloak.testsuite.migration;
import org.apache.commons.io.IOUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.constants.KerberosConstants; import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.component.PrioritizedComponentModel; import org.keycloak.component.PrioritizedComponentModel;
import org.keycloak.keys.KeyProvider; import org.keycloak.keys.KeyProvider;
@ -57,6 +61,9 @@ import org.keycloak.testsuite.exportimport.ExportImportUtil;
import org.keycloak.testsuite.runonserver.RunHelpers; import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -64,6 +71,8 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -257,6 +266,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
// MFA - Check that authentication flows were migrated as expected // MFA - Check that authentication flows were migrated as expected
testOTPAuthenticatorsMigratedToConditionalFlow(); testOTPAuthenticatorsMigratedToConditionalFlow();
testResourceTag();
} }
private void testAdminClientUrls(RealmResource realm) { private void testAdminClientUrls(RealmResource realm) {
@ -736,4 +747,16 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testDecisionStrategySetOnResourceServer(); testDecisionStrategySetOnResourceServer();
} }
} }
protected void testResourceTag() {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
URI url = suiteContext.getAuthServerInfo().getUriBuilder().path("/auth").build();
String response = SimpleHttp.doGet(url.toString(), client).asString();
Matcher m = Pattern.compile("resources/([^/]*)/welcome").matcher(response);
assertTrue(m.find());
assertTrue(m.group(1).matches("[\\da-z]{5}"));
} catch (IOException e) {
fail(e.getMessage());
}
}
} }

View file

@ -0,0 +1,70 @@
package org.keycloak.testsuite.model;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.Version;
import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.migration.MigrationModel;
import org.keycloak.models.jpa.entities.MigrationModelEntity;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.runonserver.RunOnServerTest;
import javax.persistence.EntityManager;
import java.util.List;
public class MigrationModelTest extends AbstractKeycloakTest {
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(MigrationModelTest.class);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
}
@Test
public void test() {
testingClient.server().run(session -> {
String currentVersion = Version.VERSION_KEYCLOAK.split("-")[0];
JpaConnectionProvider p = session.getProvider(JpaConnectionProvider.class);
EntityManager em = p.getEntityManager();
List<MigrationModelEntity> l = em.createQuery("select m from MigrationModelEntity m ORDER BY m.updatedTime DESC", MigrationModelEntity.class).getResultList();
Assert.assertEquals(1, l.size());
Assert.assertTrue(l.get(0).getId().matches("[\\da-z]{5}"));
Assert.assertEquals(currentVersion, l.get(0).getVersion());
MigrationModel m = session.realms().getMigrationModel();
Assert.assertEquals(currentVersion, m.getStoredVersion());
Assert.assertEquals(m.getResourcesTag(), l.get(0).getId());
Time.setOffset(-5000);
session.realms().getMigrationModel().setStoredVersion("6.0.0");
em.flush();
Time.setOffset(0);
l = em.createQuery("select m from MigrationModelEntity m ORDER BY m.updatedTime DESC", MigrationModelEntity.class).getResultList();
Assert.assertEquals(2, l.size());
Assert.assertTrue(l.get(0).getId().matches("[\\da-z]{5}"));
Assert.assertEquals(currentVersion, l.get(0).getVersion());
Assert.assertTrue(l.get(1).getId().matches("[\\da-z]{5}"));
Assert.assertEquals("6.0.0", l.get(1).getVersion());
m = session.realms().getMigrationModel();
Assert.assertEquals(l.get(0).getId(), m.getResourcesTag());
Assert.assertEquals(currentVersion, m.getStoredVersion());
em.remove(l.get(1));
});
}
}