diff --git a/src/bin/install/database.sql b/src/bin/install/database.sql index adce9abd..b0f35292 100644 --- a/src/bin/install/database.sql +++ b/src/bin/install/database.sql @@ -187,6 +187,7 @@ CREATE TABLE IF NOT EXISTS `cluster_node_epochs` ( `epoch` int(10) unsigned NOT NULL, `record` varbinary(2048) NOT NULL, `token_sealed` varbinary(4096) DEFAULT NULL, + `agent_eph_pub` binary(32) DEFAULT NULL, `nbf` int(11) NOT NULL, `exp` int(11) NOT NULL, `refresh_at` int(11) NOT NULL, diff --git a/src/migrations/database/down/035_add_cluster_epoch_eph.sql b/src/migrations/database/down/035_add_cluster_epoch_eph.sql new file mode 100644 index 00000000..e450b777 --- /dev/null +++ b/src/migrations/database/down/035_add_cluster_epoch_eph.sql @@ -0,0 +1,2 @@ +-- Reverse 035_add_cluster_epoch_eph.sql. +ALTER TABLE `cluster_node_epochs` DROP COLUMN IF EXISTS `agent_eph_pub`; diff --git a/src/migrations/database/up/035_add_cluster_epoch_eph.sql b/src/migrations/database/up/035_add_cluster_epoch_eph.sql new file mode 100644 index 00000000..305187a7 --- /dev/null +++ b/src/migrations/database/up/035_add_cluster_epoch_eph.sql @@ -0,0 +1,5 @@ +-- The agent's per-epoch X25519 key an epoch's token was sealed to. A refresh +-- whose reply was lost is retried with the same key, and MAIN re-sends the +-- unused next epoch instead of minting another, so a node never holds more +-- than two valid epochs. +ALTER TABLE `cluster_node_epochs` ADD COLUMN IF NOT EXISTS `agent_eph_pub` binary(32) DEFAULT NULL AFTER `token_sealed`; diff --git a/tests/Unit/ClusterSchemaTest.php b/tests/Unit/ClusterSchemaTest.php index 7eac5e23..d8640189 100644 --- a/tests/Unit/ClusterSchemaTest.php +++ b/tests/Unit/ClusterSchemaTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase; * where CI has no database. */ final class ClusterSchemaTest extends TestCase { - private const MIGRATIONS = ['028_add_cluster_settings', '029_create_cluster_nodes', '030_create_cluster_commands', '031_create_cluster_enrolment', '032_create_cluster_audit', '033_add_crontab_role', '034_create_cluster_changes']; + private const MIGRATIONS = ['028_add_cluster_settings', '029_create_cluster_nodes', '030_create_cluster_commands', '031_create_cluster_enrolment', '032_create_cluster_audit', '033_add_crontab_role', '034_create_cluster_changes', '035_add_cluster_epoch_eph']; private function src(string $rPath): string { return (string) file_get_contents(dirname(__DIR__, 2) . '/src/' . $rPath); @@ -30,6 +30,25 @@ final class ClusterSchemaTest extends TestCase { public function testClusterTablesMatchDatabaseSql(): void { $rInstall = $this->tables($this->src('bin/install/database.sql')); + // Columns a later migration ALTERs into a table: present in database.sql, + // absent from the migration that created the table. + $rAdded = []; + foreach (self::MIGRATIONS as $rName) { + if (preg_match_all('/ALTER TABLE `([a-z_]+)` ADD COLUMN IF NOT EXISTS `([a-z_]+)`/', $this->src('migrations/database/up/' . $rName . '.sql'), $rM, PREG_SET_ORDER)) { + foreach ($rM as [, $rTable, $rColumn]) { + $rAdded[$rTable][] = $rColumn; + } + } + } + foreach ($rAdded as $rTable => $rColumns) { + if (!isset($rInstall[$rTable])) { + continue; + } + foreach ($rColumns as $rColumn) { + $this->assertStringContainsString('`' . $rColumn . '`', $rInstall[$rTable], $rTable . '.' . $rColumn); + $rInstall[$rTable] = (string) preg_replace('/\n\s*`' . $rColumn . '` [^\n]*/', '', $rInstall[$rTable]); + } + } $rCount = 0; foreach (self::MIGRATIONS as $rName) { foreach ($this->tables($this->src('migrations/database/up/' . $rName . '.sql')) as $rTable => $rBody) {