Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 54 additions & 0 deletions sql/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
24 changes: 22 additions & 2 deletions sql/upgrade/07_lowercase_zone_names.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<zone>." 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
Expand All @@ -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
Expand Down
Loading