diff --git a/Cargo.lock b/Cargo.lock index 34e4f21..1e03155 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.19", + "thiserror", "time", ] @@ -117,9 +117,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -128,9 +128,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -145,6 +145,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -217,9 +223,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "9066c49992464636f92905fa096ec58baaa4d57ec19a5c096c68d3e25ef3d136" dependencies = [ "find-msvc-tools", "jobserver", @@ -467,8 +473,23 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto 0.3.0", "rustc_version", "subtle", "zeroize", @@ -501,9 +522,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "deflate64" @@ -518,7 +539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", - "pem-rfc7468", + "pem-rfc7468 0.7.0", "zeroize", ] @@ -529,6 +550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -608,24 +630,25 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ed25519" -version = "2.2.3" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", + "pkcs8 0.11.0", + "signature 3.0.0", ] [[package]] name = "ed25519-dalek" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 5.0.0", "ed25519", "serde", - "sha2 0.10.9", + "sha2 0.11.0", + "signature 3.0.0", "subtle", "zeroize", ] @@ -710,10 +733,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "fiat-crypto" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "flate2" @@ -1077,9 +1106,9 @@ checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1122,9 +1151,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "ctutils", "typenum", @@ -1173,7 +1202,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1325,9 +1354,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "itoa" @@ -1347,7 +1376,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror", "walkdir", "windows-link", ] @@ -1416,9 +1445,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -1453,9 +1482,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "bitflags", "libc", @@ -1614,7 +1643,7 @@ dependencies = [ [[package]] name = "mtp" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-client", "mtp-codec", @@ -1630,7 +1659,7 @@ dependencies = [ [[package]] name = "mtp-client" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-codec", "mtp-common", @@ -1643,9 +1672,9 @@ dependencies = [ [[package]] name = "mtp-codec" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ - "base64", + "base64 0.23.1", "byteorder", "mtp-common", "mtp-crypto", @@ -1656,20 +1685,20 @@ dependencies = [ [[package]] name = "mtp-common" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "quinn", "rustls", - "thiserror 2.0.19", + "thiserror", "wtransport", ] [[package]] name = "mtp-crypto" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ - "base64", + "base64 0.23.1", "chacha20poly1305", "ed25519-dalek", "getrandom 0.4.3", @@ -1681,7 +1710,7 @@ dependencies = [ "rustls", "serde", "sha2 0.11.0", - "thiserror 1.0.69", + "thiserror", "tokio", "zeroize", ] @@ -1689,24 +1718,24 @@ dependencies = [ [[package]] name = "mtp-files" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-crypto", "rand 0.10.2", - "thiserror 1.0.69", + "thiserror", "zeroize", ] [[package]] name = "mtp-host" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "mtp-codec", "mtp-common", "mtp-crypto", "mtp-transport", - "rand 0.8.7", + "rand 0.10.2", "tokio", "tracing", "wtransport", @@ -1715,7 +1744,7 @@ dependencies = [ [[package]] name = "mtp-transport" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "async-trait", "mtp-codec", @@ -1733,7 +1762,7 @@ dependencies = [ [[package]] name = "mtp-type-map" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "serde", "serde_yaml", @@ -1742,7 +1771,7 @@ dependencies = [ [[package]] name = "mtp-webserver" version = "0.2.0" -source = "git+https://git.methanium.net/methanium/mtp#a692bed326dbfc8eac1a05825f4a287cbab6fd3e" +source = "git+https://git.methanium.net/methanium/mtp#b067614a684eb1856bc5db7b3fd82148c036ce6b" dependencies = [ "async-trait", "bytes", @@ -1761,7 +1790,7 @@ dependencies = [ "quinn", "rand 0.10.2", "rustls", - "thiserror 2.0.19", + "thiserror", "tokio", "tokio-rustls", "tokio-stream", @@ -1859,7 +1888,7 @@ name = "omega" version = "0.1.0" dependencies = [ "ansi_term", - "base64", + "base64 0.22.1", "bytes", "dashmap", "dotenv", @@ -1874,7 +1903,7 @@ dependencies = [ "sqlx", "strum", "strum_macros", - "thiserror 2.0.19", + "thiserror", "tokio", "uuid", "zip", @@ -1943,7 +1972,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -1956,6 +1985,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2082,7 +2120,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.19", + "thiserror", "tokio", "tracing", "web-time", @@ -2107,7 +2145,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.19", + "thiserror", "tinyvec", "tracing", "web-time", @@ -2237,7 +2275,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2329,9 +2367,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -2598,7 +2636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ "digest 0.11.3", - "keccak 0.2.0", + "keccak 0.2.1", "sponge-cursor", ] @@ -2745,7 +2783,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "crc", "crossbeam-queue", @@ -2766,7 +2804,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.19", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -2818,7 +2856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -2848,7 +2886,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.19", + "thiserror", "tracing", "whoami", ] @@ -2860,7 +2898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", @@ -2885,7 +2923,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.19", + "thiserror", "tracing", "whoami", ] @@ -2909,7 +2947,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.19", + "thiserror", "tracing", "url", ] @@ -3018,33 +3056,13 @@ dependencies = [ "libc", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.19", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "thiserror-impl", ] [[package]] @@ -3060,9 +3078,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "js-sys", @@ -3133,13 +3151,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3515,7 +3533,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -3728,7 +3746,7 @@ dependencies = [ "rustls-pki-types", "sha2 0.11.0", "socket2", - "thiserror 2.0.19", + "thiserror", "time", "tokio", "tracing", @@ -3745,7 +3763,7 @@ checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" dependencies = [ "httlib-huffman", "octets", - "thiserror 2.0.19", + "thiserror", "url", ] @@ -3755,7 +3773,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", "serde", "zeroize", @@ -3776,7 +3794,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.19", + "thiserror", "time", ] @@ -3815,18 +3833,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -3936,9 +3954,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/migrations/005_nullable_user_iota.sql b/migrations/005_nullable_user_iota.sql new file mode 100644 index 0000000..66e0edb --- /dev/null +++ b/migrations/005_nullable_user_iota.sql @@ -0,0 +1,11 @@ +ALTER TABLE users + DROP FOREIGN KEY fk_users_iota; + +ALTER TABLE users + MODIFY iota_id BIGINT NULL; + +ALTER TABLE users + ADD CONSTRAINT fk_users_iota + FOREIGN KEY (iota_id) + REFERENCES iotas (id) + ON DELETE SET NULL; diff --git a/migrations/006_pending_iota_user_erasure.sql b/migrations/006_pending_iota_user_erasure.sql new file mode 100644 index 0000000..eb72c7b --- /dev/null +++ b/migrations/006_pending_iota_user_erasure.sql @@ -0,0 +1,8 @@ +CREATE TABLE pending_iota_user_erasure ( + user_id BIGINT NOT NULL, + iota_id BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, iota_id), + CONSTRAINT fk_pending_iota_user_erasure_iota + FOREIGN KEY (iota_id) REFERENCES iotas(id) ON DELETE CASCADE +); diff --git a/src/api/response.rs b/src/api/response.rs index e8489b5..965d85e 100644 --- a/src/api/response.rs +++ b/src/api/response.rs @@ -28,7 +28,7 @@ pub struct UserResponse { pub username: String, pub public_key: String, pub user_id: i64, - pub iota_id: i64, + pub iota_id: Option, pub sub_level: i32, pub sub_end: i64, #[serde(skip_serializing_if = "Option::is_none")] @@ -47,7 +47,7 @@ pub struct UsernameResponse { pub username: String, pub public_key: String, pub user_id: i64, - pub iota_id: i64, + pub iota_id: Option, pub sub_level: i32, pub sub_end: i64, } diff --git a/src/db/user_repo.rs b/src/db/user_repo.rs index 01b0e6e..df99a18 100644 --- a/src/db/user_repo.rs +++ b/src/db/user_repo.rs @@ -101,7 +101,7 @@ const USER_COLUMNS: &str = "SELECT id, iota_id, username, display, status, prese #[derive(FromRow)] struct UserRow { id: i64, - iota_id: i64, + iota_id: Option, username: Vec, display: Option>, status: Option>, @@ -124,7 +124,7 @@ impl TryFrom for User { |value| String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error))); Ok(User { id: row.id.into(), - iota_id: row.iota_id.into(), + iota_id: row.iota_id.map(IotaId::from), username: decode(row.username)?, display: row.display.map(decode).transpose()?, status: row.status.map(decode).transpose()?, @@ -344,9 +344,9 @@ pub async fn change_presence_preference(id: UserId, value: String) -> Result<()> .await } -pub async fn change_iota_id(id: UserId, value: IotaId) -> Result<()> { +pub async fn change_iota_id(id: UserId, value: Option) -> Result<()> { sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?") - .bind(value.0) + .bind(value.map(|id| id.0)) .bind(id.0) .execute(&pool().await?) .await?; @@ -368,6 +368,42 @@ pub async fn delete_user(id: UserId) -> Result<()> { Ok(()) } +/// Delete the central identity while retaining a durable instruction for the +/// last hosting Iota. The pending row is intentionally independent of users: +/// it must outlive the account row. +pub async fn delete_user_with_pending_erasure(id: UserId) -> Result> { + let mut tx = pool().await?.begin().await?; + let row = sqlx::query("SELECT iota_id FROM users WHERE id = ? FOR UPDATE") + .bind(id.0) + .fetch_optional(&mut *tx) + .await? + .ok_or(OmegaError::NotFound)?; + let iota_id: Option = row.get("iota_id"); + if let Some(iota_id) = iota_id { + sqlx::query("INSERT IGNORE INTO pending_iota_user_erasure (user_id, iota_id) VALUES (?, ?)") + .bind(id.0) + .bind(iota_id) + .execute(&mut *tx) + .await?; + } + sqlx::query("DELETE FROM registration_leases WHERE user_id = ?").bind(id.0).execute(&mut *tx).await?; + sqlx::query("DELETE FROM users WHERE id = ?").bind(id.0).execute(&mut *tx).await?; + tx.commit().await?; + Ok(iota_id.map(IotaId::from)) +} + +pub async fn pending_erasures_for_iota(iota_id: IotaId) -> Result> { + let rows = sqlx::query("SELECT user_id FROM pending_iota_user_erasure WHERE iota_id = ?") + .bind(iota_id.0).fetch_all(&pool().await?).await?; + Ok(rows.into_iter().map(|row| UserId::from(row.get::("user_id"))).collect()) +} + +pub async fn acknowledge_pending_erasure(user_id: UserId, iota_id: IotaId) -> Result { + let result = sqlx::query("DELETE FROM pending_iota_user_erasure WHERE user_id = ? AND iota_id = ?") + .bind(user_id.0).bind(iota_id.0).execute(&pool().await?).await?; + Ok(result.rows_affected() == 1) +} + pub async fn change_keys(id: UserId, public_key: PublicKeyBundle) -> Result<()> { sqlx::query("UPDATE users SET public_key = ? WHERE id = ?") .bind(public_key.as_bytes()) @@ -438,7 +474,7 @@ pub async fn register_complete_user( .map_err(OmegaError::from)? { Some(existing) - if existing.iota_id == iota_id + if existing.iota_id == Some(iota_id) && existing.username == username && existing.public_key.as_bytes() == public_key.as_bytes() && existing.token == token => diff --git a/src/models/user.rs b/src/models/user.rs index 2843b6d..a9595d9 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -4,7 +4,7 @@ use mtp::crypto::PublicKeyBundle; #[derive(Clone, Debug, serde::Serialize)] pub struct User { pub id: UserId, - pub iota_id: IotaId, + pub iota_id: Option, pub username: String, pub display: Option, pub status: Option, diff --git a/src/server/api.rs b/src/server/api.rs index 498685a..0319607 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -41,7 +41,7 @@ fn user_response(user: crate::models::User) -> UserResponse { username: user.username, public_key: user.public_key.to_base64(), user_id: user.id.0, - iota_id: user.iota_id.0, + iota_id: user.iota_id.map(|id| id.0), sub_level: user.sub_level, sub_end: user.sub_end, display: user.display, @@ -83,7 +83,15 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { omikron_id } else { let user = get_by_user_id(UserId::from(id)).await?; - get_iota_primary_omikron_connection(user.iota_id.0).ok_or(OmegaError::NotFound)? + match user.iota_id { + Some(iota_id) => get_iota_primary_omikron_connection(iota_id.0), + None => get_random_omikron() + .await + .map_err(|_| OmegaError::NotFound)? + .get_omikron_id() + .await, + } + .ok_or(OmegaError::NotFound)? }; // Database rows describe registered Omikrons. The public discovery @@ -150,7 +158,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { username: user.username, public_key: user.public_key.to_base64(), user_id: user.id.0, - iota_id: user.iota_id.0, + iota_id: user.iota_id.map(|id| id.0), sub_level: user.sub_level, sub_end: user.sub_end, }), diff --git a/src/state.rs b/src/state.rs index dc91021..075d268 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,18 +1,47 @@ use crate::sql::user_online_tracker::PresenceTracker; use std::sync::Arc; +use dashmap::DashMap; +use std::time::{Duration, Instant}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum AccountChallengeOperation { Attach, Delete } + +#[derive(Clone, Debug)] +pub struct AccountChallenge { + pub operation: AccountChallengeOperation, + pub user_id: i64, + pub requester_iota_id: i64, + pub nonce: u64, + pub created_at: Instant, +} pub struct OmegaState { pub presence: Arc, + challenges: DashMap<(AccountChallengeOperation, i64, i64), AccountChallenge>, } impl Default for OmegaState { fn default() -> Self { Self { presence: Arc::new(PresenceTracker::default()), + challenges: DashMap::new(), } } } +impl OmegaState { + pub fn issue_challenge(&self, operation: AccountChallengeOperation, user_id: i64, requester_iota_id: i64) -> u64 { + let nonce = rand::random::(); + self.challenges.insert((operation, user_id, requester_iota_id), AccountChallenge { operation, user_id, requester_iota_id, nonce, created_at: Instant::now() }); + nonce + } + + pub fn consume_challenge(&self, operation: AccountChallengeOperation, user_id: i64, requester_iota_id: i64, nonce: u64) -> bool { + self.challenges.remove(&(operation, user_id, requester_iota_id)).is_some_and(|(_, value)| + value.nonce == nonce && value.created_at.elapsed() <= Duration::from_secs(120)) + } +} + impl OmegaState { pub fn new() -> Arc { Arc::new(Self::default()) diff --git a/src/transport/handlers/account.rs b/src/transport/handlers/account.rs index 1927b14..360076d 100644 --- a/src/transport/handlers/account.rs +++ b/src/transport/handlers/account.rs @@ -2,8 +2,9 @@ use super::super::omikron_connection::{OmikronConnection, OmikronResult}; use crate::{ db::{iota_repo, user_repo}, models::{IotaId, UserId}, + state::AccountChallengeOperation, }; -use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use mtp::{codec::{CommunicationType, CommunicationValue, DataType, DataValue}, crypto::{verify_ed25519, verify_ml_dsa}}; use std::sync::Arc; async fn delete( @@ -23,12 +24,8 @@ pub async fn user( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - delete( - connection, - value.clone(), - user_repo::delete_user(UserId::from(value.get_sender() as i64)), - ) - .await + let user_id = UserId::from(value.get_sender() as i64); + complete_delete(connection, value, user_id).await } pub async fn iota( connection: Arc, @@ -41,3 +38,157 @@ pub async fn iota( ) .await } + +pub async fn release_from_iota( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let Some(user_id) = value + .get_data(DataType::UserId) + .as_signed_number() + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) + else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .await; + }; + let requester = IotaId::from(value.get_sender() as i64); + let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .await; + }; + if user.iota_id != Some(requester) { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) + .await; + } + let previous_iota = user.iota_id; + match user_repo::change_iota_id(user.id, None).await { + Ok(()) => { + if let Some(iota) = previous_iota { crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; } + connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())).await + }, + Err(error) => connection + .send(&CommunicationValue::new(CommunicationType::ErrorInternal) + .with_id(value.get_id()) + .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string()))) + .await, + } +} + +fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec { + let mut payload = Vec::with_capacity(domain.len() + 32); + payload.extend_from_slice(domain); + payload.extend_from_slice(&user_id.to_be_bytes()); + payload.extend_from_slice(&iota_id.to_be_bytes()); + payload.extend_from_slice(&nonce.to_be_bytes()); + payload +} + +pub async fn attach_begin(connection: Arc, value: CommunicationValue) -> OmikronResult<()> { + let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { + return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; + }; + if user_repo::get_by_user_id(UserId::from(user_id)).await.is_err() { + return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; + } + let requester = value.get_sender() as i64; + let nonce = connection.state().issue_challenge(AccountChallengeOperation::Attach, user_id, requester); + connection.send(&CommunicationValue::new(CommunicationType::AttachUserChallenge).with_id(value.get_id()) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) + .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into()))).await +} + +pub async fn attach_complete(connection: Arc, value: CommunicationValue) -> OmikronResult<()> { + let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; }; + let requester = value.get_sender() as i64; + let Some(nonce) = value.get_data(DataType::ServerNonce).as_signed_number().and_then(|v| u64::try_from(v).ok()) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; }; + let signature = value.get_data(DataType::Signature).as_bytes(); + let pq_signature = value.get_data(DataType::PqSignature).as_bytes(); + let (Some(signature), Some(pq_signature)) = (signature, pq_signature) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; }; + if !connection.state().consume_challenge(AccountChallengeOperation::Attach, user_id, requester, nonce) { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; } + let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; }; + let payload = lifecycle_payload(b"tensamin:user-attach:v1\0", user_id, requester, nonce); + if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err() || verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err() { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated).await; } + let previous_iota = user.iota_id; + match user_repo::change_iota_id(user.id, Some(IotaId::from(requester))).await { + Ok(()) => { + if let Some(iota) = previous_iota.filter(|id| id.0 != requester) { crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; } + crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await; + connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())).await + }, + Err(_) => connection.send_error_response(value.get_id(), CommunicationType::ErrorInternal).await, + } +} + +async fn complete_delete(connection: Arc, value: CommunicationValue, user_id: UserId) -> OmikronResult<()> { + match user_repo::delete_user_with_pending_erasure(user_id).await { + Ok(iota_id) => { + let cleanup_pending = iota_id.is_some(); + if let Some(iota_id) = iota_id { + crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id.0).await; + crate::transport::omikron_manager::deliver_pending_erasures(iota_id.0).await; + } + connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id()) + .add_typed_default(DataType::CleanupPending, DataValue::Bool(cleanup_pending))).await + } + Err(crate::error::OmegaError::NotFound) => connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await, + Err(error) => connection.send(&CommunicationValue::new(CommunicationType::ErrorInternal).with_id(value.get_id()).add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string()))).await, + } +} + +pub async fn delete_credential_begin(connection: Arc, value: CommunicationValue) -> OmikronResult<()> { + let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { + return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; + }; + if user_repo::get_by_user_id(UserId::from(user_id)).await.is_err() { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; } + let requester = value.get_sender() as i64; + let nonce = connection.state().issue_challenge(AccountChallengeOperation::Delete, user_id, requester); + connection.send(&CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge).with_id(value.get_id()) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) + .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into()))).await +} + +pub async fn delete_credential_complete(connection: Arc, value: CommunicationValue) -> OmikronResult<()> { + let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; }; + let requester = value.get_sender() as i64; + let Some(nonce) = value.get_data(DataType::ServerNonce).as_signed_number().and_then(|v| u64::try_from(v).ok()) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; }; + let (Some(signature), Some(pq_signature)) = (value.get_data(DataType::Signature).as_bytes(), value.get_data(DataType::PqSignature).as_bytes()) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; }; + if !connection.state().consume_challenge(AccountChallengeOperation::Delete, user_id, requester, nonce) { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge).await; } + let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotFound).await; }; + let payload = lifecycle_payload(b"tensamin:user-delete:v1\0", user_id, requester, nonce); + if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err() || verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err() { return connection.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated).await; } + complete_delete(connection, value, user.id).await +} + +pub async fn erase_hosted_user_data_ack(connection: Arc, value: CommunicationValue) -> OmikronResult<()> { + let Some(user_id) = value.get_data(DataType::UserId).as_signed_number().and_then(|v| i64::try_from(v).ok()).filter(|v| *v > 0) else { return connection.send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId).await; }; + let iota_id = IotaId::from(value.get_sender() as i64); + match user_repo::acknowledge_pending_erasure(UserId::from(user_id), iota_id).await { + Ok(true) => connection.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())).await, + Ok(false) => connection.send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated).await, + Err(_) => connection.send_error_response(value.get_id(), CommunicationType::ErrorInternal).await, + } +} + +/// New lifecycle operation names are intentionally fail-closed until their +/// proof and durable-erasure handlers are enabled. This explicit dispatch +/// prevents either a bare Iota request or the legacy DeleteUser path from +/// acquiring account-deletion authority during a staged rollout. +pub async fn lifecycle_unavailable( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + connection + .send( + &CommunicationValue::new(CommunicationType::ErrorNotAuthenticated) + .with_id(value.get_id()) + .add_typed_default( + DataType::ErrorType, + DataValue::Str("user lifecycle proof handler is not enabled".into()), + ), + ) + .await +} diff --git a/src/transport/handlers/presence.rs b/src/transport/handlers/presence.rs index 0c31904..de8a275 100644 --- a/src/transport/handlers/presence.rs +++ b/src/transport/handlers/presence.rs @@ -53,7 +53,7 @@ fn states_for_users(state: &OmegaState, users: &[crate::models::User]) -> HashMa user.id.0, state .presence - .resolve_public_state(user.id.0, user.iota_id.0), + .resolve_public_state(user.id.0, user.iota_id.map(|id| id.0).unwrap_or_default()), ) }) .collect() @@ -69,7 +69,7 @@ fn changed_states( .filter_map(|user| { let after = state .presence - .resolve_public_state(user.id.0, user.iota_id.0); + .resolve_public_state(user.id.0, user.iota_id.map(|id| id.0).unwrap_or_default()); (before.get(&user.id.0) != Some(&after)).then_some((user.id.0, after)) }) .collect::>(); @@ -275,7 +275,7 @@ pub async fn user_connected( Ok(preferences) => preferences, Err(error) => return Err(error.into()), }; - if user.iota_id.0 != iota_id || !state.presence.has_iota_route(iota_id) { + if user.iota_id.map(|id| id.0) != Some(iota_id) || !state.presence.has_iota_route(iota_id) { return connection .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) .await; @@ -497,6 +497,7 @@ pub async fn iota_connected( .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) .add_typed_default(DataType::UserIds, DataValue::Array(user_ids)); connection.clone().send(&response).await?; + crate::transport::omikron_manager::deliver_pending_erasures(iota_id).await; publish_changed_states(&state, &before, &users).await; connection .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) diff --git a/src/transport/handlers/states.rs b/src/transport/handlers/states.rs index 690a887..d147f27 100644 --- a/src/transport/handlers/states.rs +++ b/src/transport/handlers/states.rs @@ -110,7 +110,7 @@ pub async fn get( }; let status = state .presence - .resolve_public_state(user_id, user.iota_id.0) + .resolve_public_state(user_id, user.iota_id.map(|id| id.0).unwrap_or_default()) .to_string(); let mut map = Vec::new(); if let Some(kind) = DataType::UserId.try_to_id(&tm) { diff --git a/src/transport/handlers/user_data.rs b/src/transport/handlers/user_data.rs index 7e79bee..e331125 100644 --- a/src/transport/handlers/user_data.rs +++ b/src/transport/handlers/user_data.rs @@ -43,7 +43,7 @@ pub async fn get_user( .await; }; let id = user.id.0; - let iota_id = user.iota_id.0; + let iota_id = user.iota_id.map(|id| id.0); let username = user.username.clone(); let display = user .display @@ -57,7 +57,6 @@ pub async fn get_user( DataValue::Str(user.public_key.to_base64()), ) .add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into())) - .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) .add_typed_default(DataType::Display, DataValue::Str(display)) .add_typed_default( DataType::SubLevel, @@ -92,7 +91,7 @@ pub async fn get_user( } state.presence.resolve_private_state(id) } else { - state.presence.resolve_public_state(id, iota_id) + iota_id.map(|iota_id| state.presence.resolve_public_state(id, iota_id)).unwrap_or(crate::sql::connection_status::UserStatus::user_offline) }; response = response .add_typed_default( @@ -101,8 +100,11 @@ pub async fn get_user( ) .add_typed_default( DataType::OmikronConnections, - connections(&connection, iota_id), + iota_id.map(|iota_id| connections(&connection, iota_id)).unwrap_or_else(|| DataValue::Array(Vec::new())), ); + if let Some(iota_id) = iota_id { + response = response.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())); + } if let Some(route) = route { response = response.add_typed_default( DataType::OmikronId, @@ -123,26 +125,25 @@ pub async fn get_iota( .map(|iota| (iota.id.0, iota.public_key, None, None)) } else if let Some(id) = value.get_data(DataType::UserId).as_number() { if let Ok(user) = user_repo::get_by_user_id(UserId::from(id as i64)).await { - iota_repo::get_iota_by_id(user.iota_id) - .await - .ok() - .map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None)) + match user.iota_id { + Some(iota_id) => iota_repo::get_iota_by_id(iota_id) + .await + .ok() + .map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None)), + None => None, + } } else { None } } else if let Some(name) = value.get_data(DataType::Username).as_str() { if let Ok(user) = user_repo::get_by_username(name).await { - iota_repo::get_iota_by_id(user.iota_id) - .await - .ok() - .map(|iota| { - ( - iota.id.0, - iota.public_key, - Some(user.id.0), - Some(name.to_owned()), - ) - }) + match user.iota_id { + Some(iota_id) => iota_repo::get_iota_by_id(iota_id) + .await + .ok() + .map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), Some(name.to_owned()))), + None => None, + } } else { None } @@ -269,7 +270,7 @@ pub async fn change_iota( .await; } let result = - match user_repo::change_iota_id(user_id, IotaId::from(value.get_sender() as i64)).await { + match user_repo::change_iota_id(user_id, Some(IotaId::from(value.get_sender() as i64))).await { Ok(()) => user_repo::change_token(user_id, new_token.to_owned()).await, Err(error) => Err(error), }; diff --git a/src/transport/omikron_connection.rs b/src/transport/omikron_connection.rs index 5001ff3..7ad0136 100644 --- a/src/transport/omikron_connection.rs +++ b/src/transport/omikron_connection.rs @@ -87,6 +87,7 @@ impl OmikronConnection { &self.peer_capabilities } + pub async fn handle(self: Arc, receiver: &mut WebMtpReceiver) { log_in!( self.id as i64, @@ -215,6 +216,24 @@ impl OmikronConnection { Some(CommunicationType::DeleteUser) => { crate::transport::handlers::account::user(self, value).await } + Some(CommunicationType::AttachUserBegin) => { + crate::transport::handlers::account::attach_begin(self, value).await + } + Some(CommunicationType::AttachUserComplete) => { + crate::transport::handlers::account::attach_complete(self, value).await + } + Some(CommunicationType::DeleteUserCredentialBegin) => { + crate::transport::handlers::account::delete_credential_begin(self, value).await + } + Some(CommunicationType::DeleteUserCredentialComplete) => { + crate::transport::handlers::account::delete_credential_complete(self, value).await + } + Some(CommunicationType::EraseHostedUserDataAck) => { + crate::transport::handlers::account::erase_hosted_user_data_ack(self, value).await + } + Some(CommunicationType::ReleaseUserFromIota) => { + crate::transport::handlers::account::release_from_iota(self, value).await + } Some(CommunicationType::DeleteIota) => { crate::transport::handlers::account::iota(self, value).await } diff --git a/src/transport/omikron_manager.rs b/src/transport/omikron_manager.rs index 254ced8..9020556 100644 --- a/src/transport/omikron_manager.rs +++ b/src/transport/omikron_manager.rs @@ -3,7 +3,7 @@ use crate::state::OmegaState; use crate::transport::connection::OmikronConnection; use crate::transport::omikron_connection::OmikronResult; use dashmap::DashMap; -use mtp::codec::CommunicationValue; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use once_cell::sync::Lazy; use rand::prelude::IteratorRandom; use std::sync::Arc; @@ -65,7 +65,8 @@ pub async fn get_all_connections() for user in users { for route in state.presence.routes_for_user(user.id.0) { if let Some(iotas) = result.get_mut(&route.omikron_id) { - if let Some(users) = iotas.get_mut(&user.iota_id.0) { + if let Some(iota_id) = user.iota_id + && let Some(users) = iotas.get_mut(&iota_id.0) { users.push(user.id.0); } } @@ -113,3 +114,28 @@ pub async fn send_to_user(user_id: i64, cv: &CommunicationValue) { } } } + +/// Publish the authoritative membership list after an attach, migration, or +/// release. Omikron replaces its full local index from this snapshot. +pub async fn publish_iota_user_snapshot(iota_id: i64) { + let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) else { return; }; + let Some(connection) = get_connected_omikron(omikron_id) else { return; }; + let Ok(users) = user_repo::get_users_by_iota_id(crate::models::IotaId::from(iota_id)).await else { return; }; + let user_ids = users.into_iter().map(|user| DataValue::SignedNumber(user.id.0.into())).collect(); + let snapshot = CommunicationValue::new(CommunicationType::IotaUserData) + .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) + .add_typed_default(DataType::UserIds, DataValue::Array(user_ids)); + let _ = connection.send(&snapshot).await; +} + +pub async fn deliver_pending_erasures(iota_id: i64) { + let Ok(users) = user_repo::pending_erasures_for_iota(crate::models::IotaId::from(iota_id)).await else { return; }; + let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) else { return; }; + let Some(connection) = get_connected_omikron(omikron_id) else { return; }; + for user_id in users { + let request = CommunicationValue::new(CommunicationType::EraseHostedUserData) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.0.into())) + .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())); + let _ = connection.clone().send(&request).await; + } +}