Skip to content

Commit

Permalink
Migrate all identity_zone.subdomain values in the DB to lowercase.
Browse files Browse the repository at this point in the history
Some databases are case sensitives, but subdomains should not be.
For existing databases that may have duplicates based on case, will have a breaking change. This is highly unlikely though.

https://www.pivotaltracker.com/story/show/101469422
[#101469422]
  • Loading branch information
fhanik committed Oct 6, 2015
1 parent 223d7f0 commit 144415a
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 56 deletions.
@@ -0,0 +1,94 @@
/*
* *****************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
* *****************************************************************************
*/

package org.cloudfoundry.identity.uaa.db;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning;
import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning;
import org.flywaydb.core.api.migration.spring.SpringJdbcMigration;
import org.flywaydb.core.internal.util.StringUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class StoreSubDomainAsLowerCase_V2_7_3 implements SpringJdbcMigration {

Log logger = LogFactory.getLog(StoreSubDomainAsLowerCase_V2_7_3.class);

@Override
public synchronized void migrate(JdbcTemplate jdbcTemplate) throws Exception {
RandomValueStringGenerator generator = new RandomValueStringGenerator(3);
IdentityZoneProvisioning provisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate);
Map<String, List<IdentityZone>> zones = new HashMap<>();
Set<String> duplicates = new HashSet<>();
for (IdentityZone zone : provisioning.retrieveAll()) {
addToMap(zone, zones, duplicates);
}
for (String s : duplicates) {
logger.debug("Processing zone duplicates for subdomain:" + s);
List<IdentityZone> dupZones = zones.get(s);
for (int i=1; dupZones.size()>1 && i<dupZones.size(); i++) {
IdentityZone dupZone = dupZones.get(i);
String newsubdomain = null;
while (newsubdomain==null) {
String potentialsubdomain = (dupZone.getSubdomain() +"-"+ generator.generate()).toLowerCase();
if (zones.get(potentialsubdomain)==null) {
newsubdomain = potentialsubdomain;
}
}
logger.debug(String.format("Updating zone id:%s; old subdomain: %s; new subdomain: %s;", dupZone.getId(), dupZone.getSubdomain(), newsubdomain));
dupZone.setSubdomain(newsubdomain);
dupZone = provisioning.update(dupZone);
zones.put(newsubdomain, Arrays.asList(dupZone));
}
}
for (IdentityZone zone : provisioning.retrieveAll()) {
String subdomain = zone.getSubdomain();
if (StringUtils.hasText(subdomain) && !(subdomain.toLowerCase().equals(subdomain))) {
logger.debug(String.format("Lowercasing zone subdomain for id:%s; old subdomain: %s; new subdomain: %s;", zone.getId(), zone.getSubdomain(), zone.getSubdomain().toLowerCase()));
zone.setSubdomain(subdomain.toLowerCase());
provisioning.update(zone);
}

}
}

private void addToMap(IdentityZone zone, Map<String, List<IdentityZone>> zones, Set<String> duplicates) {
if (zone==null || zone.getSubdomain()==null) {
return;
}
String subdomain = zone.getSubdomain().toLowerCase();
if (zones.get(subdomain)==null) {
List<IdentityZone> list = new LinkedList<>();
list.add(zone);
zones.put(subdomain, list);
} else {
logger.warn("Found duplicate zone for subdomain:"+subdomain);
duplicates.add(subdomain);
zones.get(subdomain).add(zone);
}
}


}
@@ -0,0 +1,21 @@
/*
* ******************************************************************************
* * Cloud Foundry
* * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* *
* * This product is licensed to you under the Apache License, Version 2.0 (the "License").
* * You may not use this product except in compliance with the License.
* *
* * This product includes a number of subcomponents with
* * separate copyright notices and license terms. Your use of these
* * subcomponents is subject to the terms and conditions of the
* * subcomponent's license, as noted in the LICENSE file.
* ******************************************************************************
*/

package org.cloudfoundry.identity.uaa.db.hsqldb;

import org.cloudfoundry.identity.uaa.db.StoreSubDomainAsLowerCase_V2_7_3;

public class V2_7_3__Migrate_Zone_Subdomain_To_Lowercase extends StoreSubDomainAsLowerCase_V2_7_3 {
}
@@ -0,0 +1,21 @@
/*
* ******************************************************************************
* * Cloud Foundry
* * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* *
* * This product is licensed to you under the Apache License, Version 2.0 (the "License").
* * You may not use this product except in compliance with the License.
* *
* * This product includes a number of subcomponents with
* * separate copyright notices and license terms. Your use of these
* * subcomponents is subject to the terms and conditions of the
* * subcomponent's license, as noted in the LICENSE file.
* ******************************************************************************
*/

package org.cloudfoundry.identity.uaa.db.mysql;

import org.cloudfoundry.identity.uaa.db.StoreSubDomainAsLowerCase_V2_7_3;

public class V2_7_3__Migrate_Zone_Subdomain_To_Lowercase extends StoreSubDomainAsLowerCase_V2_7_3 {
}
@@ -0,0 +1,21 @@
/*
* ******************************************************************************
* * Cloud Foundry
* * Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* *
* * This product is licensed to you under the Apache License, Version 2.0 (the "License").
* * You may not use this product except in compliance with the License.
* *
* * This product includes a number of subcomponents with
* * separate copyright notices and license terms. Your use of these
* * subcomponents is subject to the terms and conditions of the
* * subcomponent's license, as noted in the LICENSE file.
* ******************************************************************************
*/

package org.cloudfoundry.identity.uaa.db.postgresql;

import org.cloudfoundry.identity.uaa.db.StoreSubDomainAsLowerCase_V2_7_3;

public class V2_7_3__Migrate_Zone_Subdomain_To_Lowercase extends StoreSubDomainAsLowerCase_V2_7_3 {
}
Expand Up @@ -35,11 +35,11 @@ public class JdbcIdentityZoneProvisioning implements IdentityZoneProvisioning {
public static final String CREATE_IDENTITY_ZONE_SQL = "insert into identity_zone(" + ID_ZONE_FIELDS + ") values (?,?,?,?,?,?,?)";

public static final String UPDATE_IDENTITY_ZONE_SQL = "update identity_zone set " + ID_ZONE_UPDATE_FIELDS + " where id=?";

public static final String IDENTITY_ZONES_QUERY = "select " + ID_ZONE_FIELDS + " from identity_zone ";

public static final String IDENTITY_ZONE_BY_ID_QUERY = IDENTITY_ZONES_QUERY + "where id=?";

public static final String IDENTITY_ZONE_BY_SUBDOMAIN_QUERY = "select " + ID_ZONE_FIELDS + " from identity_zone " + "where subdomain=?";

protected final JdbcTemplate jdbcTemplate;
Expand All @@ -60,15 +60,18 @@ public IdentityZone retrieve(String id) {
throw new ZoneDoesNotExistsException("Zone["+id+"] not found.", x);
}
}

@Override
public List<IdentityZone> retrieveAll() {
return jdbcTemplate.query(IDENTITY_ZONES_QUERY, mapper);
}

@Override
public IdentityZone retrieveBySubdomain(String subdomain) {
IdentityZone identityZone = jdbcTemplate.queryForObject(IDENTITY_ZONE_BY_SUBDOMAIN_QUERY, mapper, subdomain);
if (subdomain==null) {
throw new EmptyResultDataAccessException("Subdomain cannot be null", 1);
}
IdentityZone identityZone = jdbcTemplate.queryForObject(IDENTITY_ZONE_BY_SUBDOMAIN_QUERY, mapper, subdomain.toLowerCase());
return identityZone;
}

Expand All @@ -84,7 +87,7 @@ public void setValues(PreparedStatement ps) throws SQLException {
ps.setTimestamp(3, new Timestamp(new Date().getTime()));
ps.setTimestamp(4, new Timestamp(new Date().getTime()));
ps.setString(5, identityZone.getName());
ps.setString(6, identityZone.getSubdomain());
ps.setString(6, identityZone.getSubdomain().toLowerCase());
ps.setString(7, identityZone.getDescription());
}
});
Expand All @@ -105,7 +108,7 @@ public void setValues(PreparedStatement ps) throws SQLException {
ps.setInt(1, identityZone.getVersion() + 1);
ps.setTimestamp(2, new Timestamp(new Date().getTime()));
ps.setString(3, identityZone.getName());
ps.setString(4, identityZone.getSubdomain());
ps.setString(4, identityZone.getSubdomain().toLowerCase());
ps.setString(5, identityZone.getDescription());
ps.setString(6, identityZone.getId().trim());
}
Expand All @@ -117,7 +120,7 @@ public void setValues(PreparedStatement ps) throws SQLException {
return retrieve(identityZone.getId());
}

private static final class IdentityZoneRowMapper implements RowMapper<IdentityZone> {
public static final class IdentityZoneRowMapper implements RowMapper<IdentityZone> {
@Override
public IdentityZone mapRow(ResultSet rs, int rowNum) throws SQLException {

Expand Down
@@ -0,0 +1,147 @@
/*
* *****************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
* *****************************************************************************
*/

package org.cloudfoundry.identity.uaa.db;

import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning;
import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning;
import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;

import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeTrue;

public class StoreSubDomainAsLowerCase_V2_7_3_Tests extends JdbcTestBase {

private IdentityZoneProvisioning provisioning;
private StoreSubDomainAsLowerCase_V2_7_3 migration;
private RandomValueStringGenerator generator;

@Before
public void setUpDuplicateZones() {
provisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate);
migration = new StoreSubDomainAsLowerCase_V2_7_3();
generator = new RandomValueStringGenerator(6);
}

@Test
public void ensure_that_subdomains_get_lower_cased() throws Exception {
List<String> subdomains = Arrays.asList(
"Zone1" + generator.generate(),
"Zone2" + generator.generate(),
"Zone3" + generator.generate(),
"Zone4+generator.generate()"
);

for (String subdomain : subdomains) {
IdentityZone zone = MultitenancyFixture.identityZone(subdomain, subdomain);
IdentityZone created = provisioning.create(zone);
assertEquals(subdomain.toLowerCase(), created.getSubdomain());
jdbcTemplate.update("UPDATE identity_zone SET subdomain = ? WHERE id = ?", subdomain, subdomain);
assertEquals(subdomain, jdbcTemplate.queryForObject("SELECT subdomain FROM identity_zone where id = ?", String.class, subdomain));
}

migration.migrate(jdbcTemplate);
for (String subdomain : subdomains) {
for (IdentityZone zone :
Arrays.asList(
provisioning.retrieve(subdomain),
provisioning.retrieveBySubdomain(subdomain.toLowerCase()),
provisioning.retrieveBySubdomain(subdomain)
)
) {
assertNotNull(zone);
assertEquals(subdomain, zone.getId());
assertEquals(subdomain.toLowerCase(), zone.getSubdomain());
}
}
}

@Test
public void test_duplicate_subdomains() throws Exception {
check_db_is_case_sensitive();
List<String> ids = Arrays.asList(
"id1"+generator.generate().toLowerCase(),
"id2"+generator.generate().toLowerCase(),
"id3"+generator.generate().toLowerCase(),
"id4"+generator.generate().toLowerCase(),
"id5"+generator.generate().toLowerCase()
);
List<String> subdomains = Arrays.asList(
"domain1",
"Domain1",
"doMain1",
"domain4"+generator.generate().toLowerCase(),
"domain5"+generator.generate().toLowerCase()
);
for (int i=0; i<ids.size(); i++) {
IdentityZone zone = MultitenancyFixture.identityZone(ids.get(i), subdomains.get(i));
zone.setSubdomain(subdomains.get(i)); //mixed case
createIdentityZoneThroughSQL(zone);
}
IdentityZone lowercase = provisioning.retrieveBySubdomain("domain1");
IdentityZone mixedcase = provisioning.retrieveBySubdomain("Domain1");
assertEquals(lowercase.getId(), mixedcase.getId());

migration.migrate(jdbcTemplate);

for (IdentityZone zone : provisioning.retrieveAll()) {
//ensure we converted to lower case
assertEquals(zone.getSubdomain().toLowerCase(), zone.getSubdomain());
}
}


public void check_db_is_case_sensitive() throws Exception {
String usubdomain = "TEST_UPPER_" + generator.generate();
String lsubdomain = usubdomain.toLowerCase();

//check if the DB is case sensitive
for (String subdomain : Arrays.asList(usubdomain, lsubdomain)) {
try {
IdentityZone identityZone = MultitenancyFixture.identityZone(subdomain+generator.generate(), subdomain);
identityZone.setSubdomain(subdomain);
createIdentityZoneThroughSQL(identityZone);
} catch (SQLException x) {
assumeTrue("DB is not case sensitive. No need for this test", false);
} catch (DuplicateKeyException x) {
assumeTrue("DB is not case sensitive. No need for this test", false);
}
}
}

protected void createIdentityZoneThroughSQL(IdentityZone identityZone) throws SQLException {
jdbcTemplate.update(JdbcIdentityZoneProvisioning.CREATE_IDENTITY_ZONE_SQL, ps -> {
ps.setString(1, identityZone.getId().trim());
ps.setInt(2, identityZone.getVersion());
ps.setTimestamp(3, new Timestamp(new Date().getTime()));
ps.setTimestamp(4, new Timestamp(new Date().getTime()));
ps.setString(5, identityZone.getName());
ps.setString(6, identityZone.getSubdomain());
ps.setString(7, identityZone.getDescription());
});
}
}

0 comments on commit 144415a

Please sign in to comment.