packages/db-migrations/databases/fxa/patches/patch-096-097.sql (168 lines of code) (raw):

SET NAMES utf8mb4 COLLATE utf8mb4_bin; CALL assertPatchLevel('96'); -- This migration adds an optional `refreshTokenId` column to the `devices` table, -- allowing OAuth clients to participate in the sync device ecosystem. -- First, in order for the migration to apply cleanly via migration tooling, -- we need to drop `FOREIGN KEY` constraints on the `devices` table. There's -- one instance of this in the old `deviceCapabilities` table which is no -- longer used and should be safe to drop entirely. DROP TABLE IF EXISTS deviceCapabilities; -- And there are two foreign keys on the `deviceCommands` table, so we might -- as well remove both while we're here. The first links to the `deviceCommandIdentifiers` -- table, from which we never delete and so which will not be affected by removal. -- The second links to `devices` with `ON DELETE CASCADE`, and a previous migration -- has added explicit deletions as a replacement. So they're both safe to drop. ALTER TABLE deviceCommands DROP FOREIGN KEY deviceCommands_ibfk_1, DROP FOREIGN KEY deviceCommands_ibfk_2, ALGORITHM = INPLACE, LOCK = NONE; -- With that, we can actually add the new columns. ALTER TABLE devices ADD COLUMN refreshTokenId BINARY(32) DEFAULT NULL, ADD CONSTRAINT UQ_devices_refreshTokenId UNIQUE (uid, refreshTokenId), ALGORITHM = INPLACE, LOCK = NONE; -- And the stored procedures to use them. CREATE PROCEDURE `createDevice_5` ( IN `inUid` BINARY(16), IN `inId` BINARY(16), IN `inSessionTokenId` BINARY(32), IN `inRefreshTokenId` BINARY(32), IN `inNameUtf8` VARCHAR(255), IN `inType` VARCHAR(16), IN `inCreatedAt` BIGINT UNSIGNED, IN `inCallbackURL` VARCHAR(255), IN `inCallbackPublicKey` CHAR(88), IN `inCallbackAuthKey` CHAR(24) ) BEGIN INSERT INTO devices( uid, id, sessionTokenId, refreshTokenId, nameUtf8, type, createdAt, callbackURL, callbackPublicKey, callbackAuthKey ) VALUES ( inUid, inId, inSessionTokenId, inRefreshTokenId, inNameUtf8, inType, inCreatedAt, inCallbackURL, inCallbackPublicKey, inCallbackAuthKey ); END; CREATE PROCEDURE `updateDevice_6` ( IN `inUid` BINARY(16), IN `inId` BINARY(16), IN `inSessionTokenId` BINARY(32), IN `inRefreshTokenId` BINARY(32), IN `inNameUtf8` VARCHAR(255), IN `inType` VARCHAR(16), IN `inCallbackURL` VARCHAR(255), IN `inCallbackPublicKey` CHAR(88), IN `inCallbackAuthKey` CHAR(24), IN `inCallbackIsExpired` BOOLEAN ) BEGIN UPDATE devices SET sessionTokenId = COALESCE(inSessionTokenId, sessionTokenId), refreshTokenId = COALESCE(inRefreshTokenId, refreshTokenId), nameUtf8 = COALESCE(inNameUtf8, nameUtf8), type = COALESCE(inType, type), callbackURL = COALESCE(inCallbackURL, callbackURL), callbackPublicKey = COALESCE(inCallbackPublicKey, callbackPublicKey), callbackAuthKey = COALESCE(inCallbackAuthKey, callbackAuthKey), callbackIsExpired = COALESCE(inCallbackIsExpired, callbackIsExpired) WHERE uid = inUid AND id = inId; END; -- Return the sessionTokenId and refreshTokenId from deleteDevice -- so the auth server can remove them from other data stores. CREATE PROCEDURE `deleteDevice_4` ( IN `uidArg` BINARY(16), IN `idArg` BINARY(16) ) BEGIN SELECT devices.sessionTokenId, devices.refreshTokenId FROM devices WHERE devices.uid = uidArg AND devices.id = idArg; DELETE devices, deviceCommands, sessionTokens, unverifiedTokens FROM devices LEFT JOIN deviceCommands ON (deviceCommands.uid = devices.uid AND deviceCommands.deviceId = devices.id) LEFT JOIN sessionTokens ON devices.sessionTokenId = sessionTokens.tokenId LEFT JOIN unverifiedTokens ON sessionTokens.tokenId = unverifiedTokens.tokenId WHERE devices.uid = uidArg AND devices.id = idArg; END; CREATE PROCEDURE `accountDevices_16` ( IN `uidArg` BINARY(16) ) BEGIN SELECT d.uid, d.id, s.tokenId AS sessionTokenId, -- Ensure we only return valid sessionToken ids d.refreshTokenId, d.nameUtf8 AS name, d.type, d.createdAt, d.callbackURL, d.callbackPublicKey, d.callbackAuthKey, d.callbackIsExpired, s.uaBrowser, s.uaBrowserVersion, s.uaOS, s.uaOSVersion, s.uaDeviceType, s.uaFormFactor, s.lastAccessTime, ci.commandName, dc.commandData FROM devices AS d -- Left join, because it might not have a sessionToken. LEFT JOIN sessionTokens AS s ON d.sessionTokenId = s.tokenId LEFT JOIN ( deviceCommands AS dc FORCE INDEX (PRIMARY) INNER JOIN deviceCommandIdentifiers AS ci FORCE INDEX (PRIMARY) ON ci.commandId = dc.commandId ) ON (dc.uid = d.uid AND dc.deviceId = d.id) WHERE d.uid = uidArg -- We don't want to return 'zombie' device records where the sessionToken -- no longer exists in the sessionTokens table. AND (s.tokenId IS NOT NULL OR d.refreshTokenId IS NOT NULL) -- For easy flattening, ensure rows are ordered by device id. ORDER BY 1, 2; END; CREATE PROCEDURE `device_3` ( IN `uidArg` BINARY(16), IN `idArg` BINARY(16) ) BEGIN SELECT d.uid, d.id, s.tokenId AS sessionTokenId, -- Ensure we only return valid sessionToken ids d.refreshTokenId, d.nameUtf8 AS name, d.type, d.createdAt, d.callbackURL, d.callbackPublicKey, d.callbackAuthKey, d.callbackIsExpired, s.uaBrowser, s.uaBrowserVersion, s.uaOS, s.uaOSVersion, s.uaDeviceType, s.uaFormFactor, s.lastAccessTime, ci.commandName, dc.commandData FROM devices AS d -- Left join, because it might not have a sessionToken. LEFT JOIN sessionTokens AS s ON d.sessionTokenId = s.tokenId LEFT JOIN ( deviceCommands AS dc FORCE INDEX (PRIMARY) INNER JOIN deviceCommandIdentifiers AS ci FORCE INDEX (PRIMARY) ON ci.commandId = dc.commandId ) ON (dc.uid = d.uid AND dc.deviceId = d.id) WHERE d.uid = uidArg AND d.id = idArg -- We don't want to return 'zombie' device records where the sessionToken -- no longer exists in the sessionTokens table. AND (s.tokenId IS NOT NULL OR d.refreshTokenId IS NOT NULL); END; UPDATE dbMetadata SET value = '97' WHERE name = 'schema-patch-level';