diff --git a/CHANGELOG.md b/CHANGELOG.md index bdcb612..41f0360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - create: allocate ids internally and reject caller-supplied ids - zone: `nameservers` holds the zone's nameserver ids on every store, and POST /zone defaults it to the group's usable nameservers - fix: one live zone per name, ignoring case and a trailing dot -- fix(sql): store existing zone names lowercase without the trailing dot +- fix(sql): lowercase existing zone names, and the zone in record names - fix: one random, self describing session row per login, expired ones swept at login, and mysql throttles the activity touch - fix: a token is valid only while its session row exists, so logout revokes it, and any authenticated request counts as activity diff --git a/sql/schema.test.js b/sql/schema.test.js index aef47b1..df36928 100644 --- a/sql/schema.test.js +++ b/sql/schema.test.js @@ -255,6 +255,60 @@ describe('sql schema', () => { } }) + it('gives record names that spell their zone its new spelling', async () => { + const migration = await fs.readFile(path.join(sqlDir, 'upgrade', '07_lowercase_zone_names.sql'), 'utf8') + const zone = `records-${Date.now()}.test` + const Zone = zone.replace('records', 'Records').replace('.test', '.TEST') + const zoneIds = [] + const addZone = async (name) => { + const [z] = await conn.query('INSERT INTO nt_zone (nt_group_id, zone) VALUES (1, ?)', [name]) + zoneIds.push(z.insertId) + return z.insertId + } + const zid = await addZone(Zone) + // a live pair 07 leaves alone, so its records must keep its spelling + const skipped = await addZone(`Pair-${zone}`) + await addZone(`pair-${zone}`) + + const names = { + apex: [zid, `${Zone}.`, `${zone}.`], + host: [zid, `WWW.${Zone}.`, `WWW.${zone}.`], + bare: [zid, Zone, zone], + relative: [zid, 'www', 'www'], + outside: [zid, `not${Zone}.`, `not${Zone}.`], + canonical: [zid, `mail.${zone}.`, `mail.${zone}.`], + inSkippedZone: [skipped, `Pair-${zone}.`, `Pair-${zone}.`], + } + const ids = {} + for (const [key, [inZone, name]] of Object.entries(names)) { + const [r] = await conn.query( + "INSERT INTO nt_zone_record (nt_zone_id, name, ttl, type_id, address) VALUES (?, ?, 60, 1, '192.0.2.1')", + [inZone, name], + ) + ids[key] = r.insertId + } + const read = async () => { + const [rows] = await conn.query( + 'SELECT nt_zone_record_id, name FROM nt_zone_record WHERE nt_zone_id IN (?)', + [zoneIds], + ) + return Object.fromEntries( + Object.entries(ids).map(([k, id]) => [k, rows.find((r) => r.nt_zone_record_id === id).name]), + ) + } + + try { + await conn.query(migration) + const after = await read() + assert.deepEqual(after, Object.fromEntries(Object.entries(names).map(([k, [, , want]]) => [k, want]))) + + await assert.doesNotReject(() => conn.query(migration), 'the migration is not re-runnable') + assert.deepEqual(await read(), after) + } finally { + await conn.query('DELETE FROM nt_zone WHERE nt_zone_id IN (?)', [zoneIds]) + } + }) + it('adds the v3 export types to an existing database, and again on a re-run', async () => { const migration = await fs.readFile( path.join(sqlDir, 'upgrade', '06_add_nameserver_export_types.sql'), diff --git a/sql/upgrade/07_lowercase_zone_names.sql b/sql/upgrade/07_lowercase_zone_names.sql index b876297..a8620e7 100644 --- a/sql/upgrade/07_lowercase_zone_names.sql +++ b/sql/upgrade/07_lowercase_zone_names.sql @@ -4,8 +4,13 @@ # the trailing dot. A zone created through the 2.x API or web UI keeps the # case it was given (2.x imports lowercase it). NicTool 3 matches names # exactly, so it can't find such a zone by name, and the zone doesn't block -# a second one of the same name. NicTool 2.42's sql/upgrade.pl does the -# same rewrite. +# a second one of the same name. +# +# Record names that spell their zone then take its new spelling. 2.x stored +# '@' as "." in the zone's own case, and powerdns-pipe and the web UI +# compare record names to the zone exactly. Absolute names keep their host's +# case (www.Example.com. becomes www.example.com.), and a bare name equal to +# the zone becomes the zone. NicTool 2.42's sql/upgrade.pl does the same. # # A live zone is left alone when another live zone has the same canonical # name. The SELECT at the end lists every live zone that shares its name with @@ -31,6 +36,21 @@ UPDATE nt_zone AND (deleted = 1 OR LOWER(TRIM(TRAILING '.' FROM zone)) NOT IN (SELECT canonical FROM nt_zone_name_taken)); +# compared with z.zone as stored, so a zone left alone above, still in +# capitals, matches no lowercased name and its records keep its spelling +UPDATE nt_zone_record r + JOIN nt_zone z ON z.nt_zone_id = r.nt_zone_id + SET r.name = CONCAT(LEFT(r.name, CHAR_LENGTH(r.name) - CHAR_LENGTH(z.zone) - 1), z.zone, '.') + WHERE (LOWER(r.name) = CONCAT(z.zone, '.') + OR RIGHT(LOWER(r.name), CHAR_LENGTH(z.zone) + 2) = CONCAT('.', z.zone, '.')) + AND RIGHT(r.name, CHAR_LENGTH(z.zone) + 1) <> CONCAT(z.zone, '.'); + +UPDATE nt_zone_record r + JOIN nt_zone z ON z.nt_zone_id = r.nt_zone_id + SET r.name = z.zone + WHERE LOWER(r.name) = z.zone + AND r.name <> z.zone; + SELECT nt_zone_id, zone AS `live zone sharing its name with another` FROM nt_zone WHERE deleted = 0